pytest
Pythonの強力なテストフレームワークで、フィクスチャ・パラメータ化・プラグインに対応し、FastAPI・Django・Flaskとのフレームワーク統合もサポート。ユニットテストから統合テストまで幅広く活用できます。
description の原文を見る
pytest - Python's most powerful testing framework with fixtures, parametrization, plugins, and framework integration for FastAPI, Django, Flask
SKILL.md 本文
pytest - Professional Python Testing
Overview
pytestは業界標準のPythonテストフレームワークで、fixture、parametrization、markers、プラグイン、FastAPI、Django、Flaskとのシームレスな統合など、強力な機能を提供します。単体テストから複雑な統合テストまで、シンプルでスケーラブルなテスティング手法を実現します。
主な機能:
- 依存性注入用のfixture システム
- データドリブンテスト向けのparametrization
- 豊富なアサーション自動検査(
self.assertEqual不要) - プラグインエコシステム(pytest-cov、pytest-asyncio、pytest-mock、pytest-django)
- Async/await サポート
- pytest-xdistによる並列テスト実行
- テストディスカバリーと組織化
- 詳細な失敗レポート
インストール:
# 基本的なpytest
pip install pytest
# よく使うプラグイン付き
pip install pytest pytest-cov pytest-asyncio pytest-mock
# FastAPI テスト用
pip install pytest httpx pytest-asyncio
# Django テスト用
pip install pytest pytest-django
# 非同期データベース用
pip install pytest-asyncio aiosqlite
基本的なテストパターン
1. シンプルなテスト関数
# test_math.py
def add(a, b):
return a + b
def test_add():
assert add(2, 3) == 5
assert add(-1, 1) == 0
assert add(0, 0) == 0
def test_add_negative():
assert add(-2, -3) == -5
テストを実行:
# すべてのテストを自動検出して実行
pytest
# 詳細出力
pytest -v
# printステートメントを表示
pytest -s
# 特定のテストファイルを実行
pytest test_math.py
# 特定のテスト関数を実行
pytest test_math.py::test_add
2. テストクラスで組織化
# test_calculator.py
class Calculator:
def add(self, a, b):
return a + b
def multiply(self, a, b):
return a * b
class TestCalculator:
def test_add(self):
calc = Calculator()
assert calc.add(2, 3) == 5
def test_multiply(self):
calc = Calculator()
assert calc.multiply(4, 5) == 20
def test_add_negative(self):
calc = Calculator()
assert calc.add(-1, -1) == -2
3. アサーションと予期された失敗
import pytest
# 例外発生のテスト
def divide(a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
def test_divide_by_zero():
with pytest.raises(ValueError, match="Cannot divide by zero"):
divide(10, 0)
def test_divide_success():
assert divide(10, 2) == 5.0
# 近似値の等値性テスト
def test_float_comparison():
assert 0.1 + 0.2 == pytest.approx(0.3)
# リスト要素の確認テスト
def test_list_contains():
result = [1, 2, 3, 4]
assert 3 in result
assert len(result) == 4
Fixture - 依存性注入
基本的なFixture
# conftest.py
import pytest
@pytest.fixture
def sample_data():
"""テスト用のサンプルデータを提供."""
return {"name": "Alice", "age": 30, "email": "alice@example.com"}
@pytest.fixture
def empty_list():
"""空のリストを提供."""
return []
# test_fixtures.py
def test_sample_data(sample_data):
assert sample_data["name"] == "Alice"
assert sample_data["age"] == 30
def test_empty_list(empty_list):
empty_list.append(1)
assert len(empty_list) == 1
Fixtureのスコープ
import pytest
# 関数スコープ(デフォルト)- 各テストごとに実行
@pytest.fixture(scope="function")
def user():
return {"id": 1, "name": "Alice"}
# クラススコープ - テストクラスごとに1回実行
@pytest.fixture(scope="class")
def database():
db = setup_database()
yield db
db.close()
# モジュールスコープ - テストモジュールごとに1回実行
@pytest.fixture(scope="module")
def api_client():
client = APIClient()
yield client
client.shutdown()
# セッションスコープ - テストセッション全体で1回実行
@pytest.fixture(scope="session")
def app_config():
return load_config()
Fixtureのセットアップとティアダウン
import pytest
import tempfile
import shutil
@pytest.fixture
def temp_directory():
"""テスト用の一時ディレクトリを作成."""
temp_dir = tempfile.mkdtemp()
print(f"
セットアップ: {temp_dir}を作成しました")
yield temp_dir # ディレクトリをテストに提供
# ティアダウン: テスト後のクリーンアップ
shutil.rmtree(temp_dir)
print(f"
ティアダウン: {temp_dir}を削除しました")
def test_file_creation(temp_directory):
file_path = f"{temp_directory}/test.txt"
with open(file_path, "w") as f:
f.write("test content")
assert os.path.exists(file_path)
Fixtureの依存関係
import pytest
@pytest.fixture
def database_connection():
"""データベース接続."""
conn = connect_to_db()
yield conn
conn.close()
@pytest.fixture
def database_session(database_connection):
"""セッションは接続に依存."""
session = create_session(database_connection)
yield session
session.rollback()
session.close()
@pytest.fixture
def user_repository(database_session):
"""ユーザーリポジトリはセッションに依存."""
return UserRepository(database_session)
def test_create_user(user_repository):
user = user_repository.create(name="Alice", email="alice@example.com")
assert user.name == "Alice"
Parametrization - データドリブンテスト
基本的なParametrization
import pytest
@pytest.mark.parametrize("a,b,expected", [
(2, 3, 5),
(5, 7, 12),
(-1, 1, 0),
(0, 0, 0),
(100, 200, 300),
])
def test_add_parametrized(a, b, expected):
assert add(a, b) == expected
複数のパラメータ
@pytest.mark.parametrize("operation,a,b,expected", [
("add", 2, 3, 5),
("subtract", 10, 5, 5),
("multiply", 4, 5, 20),
("divide", 10, 2, 5),
])
def test_calculator_operations(operation, a, b, expected):
calc = Calculator()
result = getattr(calc, operation)(a, b)
assert result == expected
IDを持つParametrize
@pytest.mark.parametrize("input_data,expected", [
pytest.param({"name": "Alice"}, "Alice", id="valid_name"),
pytest.param({"name": ""}, None, id="empty_name"),
pytest.param({}, None, id="missing_name"),
], ids=lambda x: x if isinstance(x, str) else None)
def test_extract_name(input_data, expected):
result = extract_name(input_data)
assert result == expected
間接的なParametrization(Fixture)
@pytest.fixture
def user_data(request):
"""パラメータに基づくユーザーを作成."""
return {"name": request.param, "email": f"{request.param}@example.com"}
@pytest.mark.parametrize("user_data", ["Alice", "Bob", "Charlie"], indirect=True)
def test_user_creation(user_data):
assert "@example.com" in user_data["email"]
テストマーカー
ビルトインマーカー
import pytest
# テストをスキップ
@pytest.mark.skip(reason="まだ実装されていません")
def test_future_feature():
pass
# 条件付きでスキップ
@pytest.mark.skipif(sys.platform == "win32", reason="Unix限定のテスト")
def test_unix_specific():
pass
# 予期された失敗
@pytest.mark.xfail(reason="既知のバグ #123")
def test_known_bug():
assert False
# 遅いテストマーカー
@pytest.mark.slow
def test_expensive_operation():
time.sleep(5)
assert True
カスタムマーカー
# pytest.ini
[pytest]
markers =
slow: 遅いテストとしてマーク('-m "not slow"'で除外)
integration: 統合テストとしてマーク
unit: 単体テストとしてマーク
smoke: スモークテストとしてマーク
# test_custom_markers.py
import pytest
@pytest.mark.unit
def test_fast_unit():
assert True
@pytest.mark.integration
@pytest.mark.slow
def test_slow_integration():
# データベースを使う統合テスト
pass
@pytest.mark.smoke
def test_critical_path():
# 重要な機能用スモークテスト
pass
マーカー別のテスト実行:
# 単体テストのみを実行
pytest -m unit
# 遅いテスト以外を実行
pytest -m "not slow"
# 統合テストを実行
pytest -m integration
# 単体テストと統合テストを実行
pytest -m "unit or integration"
# スモークテストのみを実行
pytest -m smoke
FastAPI テスト
FastAPI テストの基本設定
# app/main.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
price: float
@app.get("/")
def read_root():
return {"message": "Hello World"}
@app.get("/items/{item_id}")
def read_item(item_id: int):
if item_id == 0:
raise HTTPException(status_code=404, detail="Item not found")
return {"item_id": item_id, "name": f"Item {item_id}"}
@app.post("/items")
def create_item(item: Item):
return {"name": item.name, "price": item.price, "id": 123}
FastAPI テストクライアント
# conftest.py
import pytest
from fastapi.testclient import TestClient
from app.main import app
@pytest.fixture
def client():
"""FastAPIテストクライアント."""
return TestClient(app)
# test_api.py
def test_read_root(client):
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello World"}
def test_read_item(client):
response = client.get("/items/1")
assert response.status_code == 200
assert response.json() == {"item_id": 1, "name": "Item 1"}
def test_read_item_not_found(client):
response = client.get("/items/0")
assert response.status_code == 404
assert response.json() == {"detail": "Item not found"}
def test_create_item(client):
response = client.post(
"/items",
json={"name": "Widget", "price": 9.99}
)
assert response.status_code == 200
data = response.json()
assert data["name"] == "Widget"
assert data["price"] == 9.99
assert "id" in data
非同期FastAPI テスト
# conftest.py
import pytest
from httpx import AsyncClient
from app.main import app
@pytest.fixture
async def async_client():
"""FastAPI用の非同期テストクライアント."""
async with AsyncClient(app=app, base_url="http://test") as client:
yield client
# test_async_api.py
import pytest
@pytest.mark.asyncio
async def test_read_root_async(async_client):
response = await async_client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello World"}
@pytest.mark.asyncio
async def test_create_item_async(async_client):
response = await async_client.post(
"/items",
json={"name": "Gadget", "price": 19.99}
)
assert response.status_code == 200
assert response.json()["name"] == "Gadget"
データベース付きFastAPI テスト
# conftest.py
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.database import Base, get_db
from app.main import app
# テスト用データベース
SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
@pytest.fixture(scope="function")
def test_db():
"""テスト用データベースを作成."""
Base.metadata.create_all(bind=engine)
yield
Base.metadata.drop_all(bind=engine)
@pytest.fixture
def client(test_db):
"""データベース依存をオーバーライド."""
def override_get_db():
try:
db = TestingSessionLocal()
yield db
finally:
db.close()
app.dependency_overrides[get_db] = override_get_db
with TestClient(app) as test_client:
yield test_client
app.dependency_overrides.clear()
# test_users.py
def test_create_user(client):
response = client.post(
"/users",
json={"email": "test@example.com", "password": "secret"}
)
assert response.status_code == 200
assert response.json()["email"] == "test@example.com"
def test_read_users(client):
# ユーザーを先に作成
client.post("/users", json={"email": "user1@example.com", "password": "pass1"})
client.post("/users", json={"email": "user2@example.com", "password": "pass2"})
# ユーザーを読取
response = client.get("/users")
assert response.status_code == 200
assert len(response.json()) == 2
Django テスト
Django pytest 設定
# pytest.ini
[pytest]
DJANGO_SETTINGS_MODULE = myproject.settings
python_files = tests.py test_*.py *_tests.py
# conftest.py
import pytest
from django.conf import settings
@pytest.fixture(scope='session')
def django_db_setup():
settings.DATABASES['default'] = {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ':memory:',
}
Djangoモデルテスト
# models.py
from django.db import models
class User(models.Model):
email = models.EmailField(unique=True)
name = models.CharField(max_length=100)
is_active = models.BooleanField(default=True)
# test_models.py
import pytest
from myapp.models import User
@pytest.mark.django_db
def test_create_user():
user = User.objects.create(
email="test@example.com",
name="Test User"
)
assert user.email == "test@example.com"
assert user.is_active is True
@pytest.mark.django_db
def test_user_unique_email():
User.objects.create(email="test@example.com", name="User 1")
with pytest.raises(Exception): # IntegrityError
User.objects.create(email="test@example.com", name="User 2")
Django ビューテスト
# views.py
from django.http import JsonResponse
from django.views import View
class UserListView(View):
def get(self, request):
users = User.objects.all()
return JsonResponse({
"users": list(users.values("id", "email", "name"))
})
# test_views.py
import pytest
from django.test import Client
from myapp.models import User
@pytest.fixture
def client():
return Client()
@pytest.mark.django_db
def test_user_list_view(client):
# テストデータを作成
User.objects.create(email="user1@example.com", name="User 1")
User.objects.create(email="user2@example.com", name="User 2")
# ビューをテスト
response = client.get("/users/")
assert response.status_code == 200
data = response.json()
assert len(data["users"]) == 2
Django REST Framework テスト
# serializers.py
from rest_framework import serializers
from myapp.models import User
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'email', 'name', 'is_active']
# views.py
from rest_framework import viewsets
from myapp.models import User
from myapp.serializers import UserSerializer
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
# test_api.py
import pytest
from rest_framework.test import APIClient
from myapp.models import User
@pytest.fixture
def api_client():
return APIClient()
@pytest.mark.django_db
def test_list_users(api_client):
User.objects.create(email="user1@example.com", name="User 1")
User.objects.create(email="user2@example.com", name="User 2")
response = api_client.get("/api/users/")
assert response.status_code == 200
assert len(response.data) == 2
@pytest.mark.django_db
def test_create_user(api_client):
data = {"email": "new@example.com", "name": "New User"}
response = api_client.post("/api/users/", data)
assert response.status_code == 201
assert User.objects.filter(email="new@example.com").exists()
モッキングとパッチング
pytest-mock(pytest.fixture.mocker)
# インストール: pip install pytest-mock
# service.py
import requests
def get_user_data(user_id):
response = requests.get(f"https://api.example.com/users/{user_id}")
return response.json()
# test_service.py
def test_get_user_data(mocker):
# requests.getをモック
mock_response = mocker.Mock()
mock_response.json.return_value = {"id": 1, "name": "Alice"}
mocker.patch("requests.get", return_value=mock_response)
result = get_user_data(1)
assert result["name"] == "Alice"
クラスメソッドのモッキング
class UserService:
def get_user(self, user_id):
# データベース呼び出し
return database.fetch_user(user_id)
def get_user_name(self, user_id):
user = self.get_user(user_id)
return user["name"]
def test_get_user_name(mocker):
service = UserService()
# get_userメソッドをモック
mocker.patch.object(
service,
"get_user",
return_value={"id": 1, "name": "Alice"}
)
result = service.get_user_name(1)
assert result == "Alice"
サイドエフェクト付きのモッキング
def test_retry_on_failure(mocker):
# 最初の呼び出しは失敗、2番目は成功
mock_api = mocker.patch("requests.get")
mock_api.side_effect = [
requests.exceptions.Timeout(), # 最初の呼び出し
mocker.Mock(json=lambda: {"status": "ok"}) # 2番目の呼び出し
]
result = api_call_with_retry()
assert result["status"] == "ok"
assert mock_api.call_count == 2
呼び出しのスパイ
def test_function_called_correctly(mocker):
spy = mocker.spy(module, "function_name")
# 関数を使うコードを呼び出す
module.run_workflow()
# 呼び出されたことを確認
assert spy.call_count == 1
spy.assert_called_once_with(arg1="value", arg2=42)
カバレッジとレポート
pytest-cov 設定
# インストール
pip install pytest-cov
# カバレッジ付きで実行
pytest --cov=app --cov-report=html --cov-report=term
# カバレッジレポートを生成
pytest --cov=app --cov-report=term-missing
# 最小カバレッジ閾値付き
pytest --cov=app --cov-fail-under=80
pytest.ini カバレッジ設定
# pytest.ini
[pytest]
addopts =
--cov=app
--cov-report=html
--cov-report=term-missing
--cov-fail-under=80
-v
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
カバレッジレポート
# HTMLレポート(ブラウザで開く)
pytest --cov=app --cov-report=html
open htmlcov/index.html
# 不足行を表示したターミナルレポート
pytest --cov=app --cov-report=term-missing
# XMLレポート(CI/CD用)
pytest --cov=app --cov-report=xml
# JSONレポート
pytest --cov=app --cov-report=json
非同期テスト
pytest-asyncio
# インストール: pip install pytest-asyncio
# conftest.py
import pytest
# asyncioモードを有効化
pytest_plugins = ('pytest_asyncio',)
# async_service.py
import asyncio
import aiohttp
async def fetch_data(url):
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
return await response.json()
# test_async_service.py
import pytest
@pytest.mark.asyncio
async def test_fetch_data(mocker):
# aiohttpレスポンスをモック
mock_response = mocker.AsyncMock()
mock_response.json.return_value = {"data": "test"}
mock_session = mocker.AsyncMock()
mock_session.__aenter__.return_value.get.return_value.__aenter__.return_value = mock_response
mocker.patch("aiohttp.ClientSession", return_value=mock_session)
result = await fetch_data("https://api.example.com/data")
assert result["data"] == "test"
非同期Fixture
@pytest.fixture
async def async_db_session():
"""非同期データベースセッション."""
async with async_engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
async with AsyncSession(async_engine) as session:
yield session
async with async_engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
@pytest.mark.asyncio
async def test_create_user_async(async_db_session):
user = User(email="test@example.com", name="Test")
async_db_session.add(user)
await async_db_session.commit()
result = await async_db_session.execute(
select(User).where(User.email == "test@example.com")
)
assert result.scalar_one().name == "Test"
ローカルpytest プロファイル(あなたのリポジトリ)
あなたのプロジェクトのpyproject.tomlから、よく使う設定:
asyncio_mode = "auto"(mcp-browser、mcp-memory、claude-mpm、edgarのデフォルト)addoptsに--strict-markersと--strict-configを含む(CI一貫性用)- カバレッジフラグ:
--cov=<package>、--cov-report=term-missing、--cov-report=xml - 選別的な無視(mcp-vector-search):
--ignore=tests/manual、--ignore=tests/e2e pythonpath = ["src"](編集可能なインポート解決用、mcp-ticketer)
一般的なマーカー:
unit、integration、e2eslow、benchmark、performancerequires_api(edgar)
参照: claude-mpm、edgar、mcp-vector-search、mcp-ticketer、kuzu-memoryのpyproject.tomlを参照してください。
ベストプラクティス
1. テスト組織
project/
├── app/
│ ├── __init__.py
│ ├── main.py
│ ├── models.py
│ └── services.py
├── tests/
│ ├── __init__.py
│ ├── conftest.py # 共有fixture
│ ├── test_models.py # モデルテスト
│ ├── test_services.py # サービステスト
│ ├── test_api.py # APIテスト
│ └── integration/
│ ├── __init__.py
│ └── test_workflows.py
└── pytest.ini
2. 命名規則
# ✅ 良い例: 明確なテスト名
def test_user_creation_with_valid_email():
pass
def test_user_creation_raises_error_for_duplicate_email():
pass
# ❌ 悪い例: 曖昧な名前
def test_user1():
pass
def test_case2():
pass
3. Arrange-Act-Assertパターン
def test_user_service_creates_user():
# Arrange: テストデータと依存性をセットアップ
service = UserService(database=mock_db)
user_data = {"email": "test@example.com", "name": "Test"}
# Act: テストされるアクションを実行
result = service.create_user(user_data)
# Assert: 結果を検証
assert result.email == "test@example.com"
assert result.id is not None
4. 共通セットアップはFixtureを使用
# ❌ 悪い例: 繰り返されたセットアップ
def test_user_creation():
db = setup_database()
user = create_user(db)
assert user.id is not None
db.close()
def test_user_deletion():
db = setup_database()
user = create_user(db)
delete_user(db, user.id)
db.close()
# ✅ 良い例: Fixture ベースのセットアップ
@pytest.fixture
def db():
database = setup_database()
yield database
database.close()
@pytest.fixture
def user(db):
return create_user(db)
def test_user_creation(user):
assert user.id is not None
def test_user_deletion(db, user):
delete_user(db, user.id)
assert not user_exists(db, user.id)
5. 類似テストはParametrizeで実装
# ❌ 悪い例: テストコードの重複
def test_add_positive():
assert add(2, 3) == 5
def test_add_negative():
assert add(-2, -3) == -5
def test_add_zero():
assert add(0, 0) == 0
# ✅ 良い例: Parametrizedテスト
@pytest.mark.parametrize("a,b,expected", [
(2, 3, 5),
(-2, -3, -5),
(0, 0, 0),
])
def test_add(a, b, expected):
assert add(a, b) == expected
6. テストごとに1つのことをテスト
# ❌ 悪い例: 複数のことをテスト
def test_user_workflow():
user = create_user()
assert user.id is not None
updated = update_user(user.id, name="New Name")
assert updated.name == "New Name"
deleted = delete_user(user.id)
assert deleted is True
# ✅ 良い例: 別々のテスト
def test_user_creation():
user = create_user()
assert user.id is not None
def test_user_update():
user = create_user()
updated = update_user(user.id, name="New Name")
assert updated.name == "New Name"
def test_user_deletion():
user = create_user()
result = delete_user(user.id)
assert result is True
7. テスト組織にマーカーを使用
@pytest.mark.unit
def test_pure_function():
pass
@pytest.mark.integration
@pytest.mark.slow
def test_database_integration():
pass
@pytest.mark.smoke
def test_critical_path():
pass
8. 外部依存をモック
# ✅ 良い例: 外部APIをモック
def test_fetch_user_data(mocker):
mocker.patch("requests.get", return_value=mock_response)
result = fetch_user_data(user_id=1)
assert result["name"] == "Alice"
# ❌ 悪い例: テストで実際のAPI呼び出し
def test_fetch_user_data():
result = fetch_user_data(user_id=1) # 実際のHTTPリクエスト!
assert result["name"] == "Alice"
よくある落とし穴
❌ アンチパターン1: テストが実行順序に依存
# 間違い: テストは独立している必要があります
class TestUserWorkflow:
user_id = None
def test_create_user(self):
user = create_user()
TestUserWorkflow.user_id = user.id
def test_update_user(self):
# test_create_userが先に実行されないと失敗します!
update_user(TestUserWorkflow.user_id, name="New")
正しい実装:
@pytest.fixture
def created_user():
return create_user()
def test_create_user(created_user):
assert created_user.id is not None
def test_update_user(created_user):
update_user(created_user.id, name="New")
❌ アンチパターン2: リソースのクリーンアップなし
# 間違い: データベースがクリーンアップされていません
def test_user_creation():
db = setup_database()
user = create_user(db)
assert user.id is not None
# データベース接続が閉じられていない!
正しい実装:
@pytest.fixture
def db():
database = setup_database()
yield database
database.close() # クリーンアップ
❌ アンチパターン3: 実装の詳細をテスト
# 間違い: 内部実装をテスト
def test_user_service_uses_cache():
service = UserService()
service.get_user(1)
assert service._cache.has_key(1) # 内部キャッシュをテスト!
正しい実装:
# 動作をテスト、実装ではなく
def test_user_service_returns_user():
service = UserService()
user = service.get_user(1)
assert user.id == 1
❌ アンチパターン4: pytest機能を使わない
# 間違い: unittest アサーションを使用
import unittest
def test_addition():
result = add(2, 3)
unittest.TestCase().assertEqual(result, 5)
正しい実装:
# pytestの豊かなアサーションを使用
def test_addition():
assert add(2, 3) == 5
❌ アンチパターン5: 過度に複雑なFixture
# 間違い: Fixtureが多すぎる処理をしています
@pytest.fixture
def everything():
db = setup_db()
user = create_user(db)
session = login(user)
cache = setup_cache()
# ... 多すぎる処理!
return {"db": db, "user": user, "session": session, "cache": cache}
正しい実装:
# 分離できる、組み合わせ可能なFixture
@pytest.fixture
def db():
return setup_db()
@pytest.fixture
def user(db):
return create_user(db)
@pytest.fixture
def session(user):
return login(user)
クイックリファレンス
よく使うコマンド
# すべてのテストを実行
pytest
# 詳細出力
pytest -v
# print ステートメントを表示
pytest -s
# 特定のファイルを実行
pytest tests/test_api.py
# 特定のテストを実行
pytest tests/test_api.py::test_create_user
# マーカーで実行
pytest -m unit
pytest -m "not slow"
# カバレッジ付きで実行
pytest --cov=app --cov-report=html
# 並列実行
pytest -n auto # pytest-xdistが必要
# 最初の失敗で停止
pytest -x
# 失敗時にローカル変数を表示
pytest -l
# 前回失敗したテストを実行
pytest --lf
# 失敗したテストを先に実行
pytest --ff
pytest.ini テンプレート
[pytest]
# 最小pytestバージョン
minversion = 7.0
# テストディスカバリーパターン
python_files = test_*.py *_test.py
python_classes = Test*
python_functions = test_*
# テストパス
testpaths = tests
# コマンドラインオプション
addopts =
-v
--strict-markers
--cov=app
--cov-report=html
--cov-report=term-missing
--cov-fail-under=80
# マーカー
markers =
unit: 単体テスト
integration: 統合テスト
slow: 実行時間が長いテスト
smoke: 重要なパスのスモークテスト
# Django設定(Djangoを使う場合)
DJANGO_SETTINGS_MODULE = myproject.settings
# Asyncioモード
asyncio_mode = auto
conftest.py テンプレート
# conftest.py
import pytest
from fastapi.testclient import TestClient
from app.main import app
# FastAPI クライアントfixture
@pytest.fixture
def client():
return TestClient(app)
# データベースfixture
@pytest.fixture(scope="function")
def db():
database = setup_test_database()
yield database
database.close()
# モックユーザーfixture
@pytest.fixture
def mock_user():
return {"id": 1, "email": "test@example.com", "name": "Test User"}
# カスタムpytest設定
def pytest_configure(config):
config.addinivalue_line("markers", "api: APIテスト")
config.addinivalue_line("markers", "db: データベーステスト")
リソース
- 公式ドキュメント: https://docs.pytest.org/
- pytest-asyncio: https://pytest-asyncio.readthedocs.io/
- pytest-cov: https://pytest-cov.readthedocs.io/
- pytest-mock: https://pytest-mock.readthedocs.io/
- pytest-django: https://pytest-django.readthedocs.io/
- FastAPI テスト: https://fastapi.tiangolo.com/tutorial/testing/
関連スキル
pytestを使う際は、以下の補足スキルを検討してください:
- fastapi-local-dev: FastAPI開発サーバーパターンとテストfixture
- test-driven-development: 完全なTDDワークフロー(RED/GREEN/REFACTOR サイクル)
- systematic-debugging: テスト失敗の根本原因調査
クイックTDDワークフロー参考(スタンドアロン使用向けにインライン化)
RED → GREEN → REFACTOR サイクル:
-
RED フェーズ: 失敗するテストを記述
def test_should_authenticate_user_when_credentials_valid(): # 目的の動作を説明するテスト user = User(username='alice', password='secret123') result = authenticate(user) assert result.is_authenticated is True # このテストは失敗します。authenticate()がまだ存在しないため -
GREEN フェーズ: テストをパスさせる
def authenticate(user): # テストをパスさせるための最小限のコード if user.username == 'alice' and user.password == 'secret123': return AuthResult(is_authenticated=True) return AuthResult(is_authenticated=False) -
REFACTOR フェーズ: コードを改善
def authenticate(user): # テストが緑色のまま、きれいなコードに hashed_password = hash_password(user.password) stored_user = database.get_user(user.username) return AuthResult( is_authenticated=(stored_user.password_hash == hashed_password) )
テスト構造: Arrange-Act-Assert(AAA)
def test_user_creation():
# Arrange: テストデータをセットアップ
user_data = {'username': 'alice', 'email': 'alice@example.com'}
# Act: アクションを実行
user = create_user(user_data)
# Assert: 結果を検証
assert user.username == 'alice'
assert user.email == 'alice@example.com'
クイックデバッグ参考(スタンドアロン使用向けにインライン化)
フェーズ1: 根本原因調査
- エラーメッセージを完全に読む(スタックトレース、行番号)
- 一貫性を確認(実行手順を記録)
- 最近の変更をチェック(git log、git diff)
- 何が変わったか、失敗の原因になった理由を理解
フェーズ2: 問題を隔離
# pytestの組み込みデバッグを使用
pytest tests/test_auth.py -vv --pdb # 失敗時にデバッガーにドロップイン
pytest tests/test_auth.py -x # 最初の失敗で停止
pytest tests/test_auth.py -k "auth" # 認証関連のテストのみ実行
# 戦略的なprint/ロギングを追加
def test_complex_workflow():
user = create_user({'username': 'test'})
print(f"DEBUG: ユーザー {user.id} を作成しました") # pytest -sで表示
result = process_user(user)
print(f"DEBUG: 結果のステータス {result.status}")
assert result.success
フェーズ3: 根本原因を修正
- 症状ではなく、根本的な問題を修正
- 再発を防ぐために回帰テストを追加
- 修正により他のテストが壊れないことを確認
フェーズ4: ソリューションを検証
# テストスイート全体を実行
pytest
# カバレッジ付きで実行
pytest --cov=src --cov-report=html
# 特定のテストパターンを検証
pytest -k "auth or login" -v
[デプロイ時に一緒に使える場合、TDDとデバッグの完全なワークフローは対応するスキルで利用できます]
pytest バージョン互換性: このスキルはpytest 7.0以上をカバーしており、2025年のPythonテストの現在のベストプラクティスを反映しています。
ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ
詳細情報
- 作者
- bobmatnyc
- ライセンス
- MIT
- 最終更新
- 不明
Source: https://github.com/bobmatnyc/claude-mpm-skills / ライセンス: MIT
関連スキル
doubt-driven-development
重要な判断はすべて、本番環境への展開前に新しい視点から対抗的レビューを実施します。速度より正確性が重要な場合、不慣れなコードを扱う場合、本番環境・セキュリティに関わるロジック・取り消し不可の操作など影響度が高い場合、または後でバグを修正するよりも今検証する方が効率的な場合に活用してください。
apprun-skills
TypeScriptを使用したAppRunアプリケーションのMVU設計に関する総合的なガイダンスが得られます。コンポーネントパターン、イベントハンドリング、状態管理(非同期ジェネレータを含む)、パラメータと保護機能を備えたルーティング・ナビゲーション、vistestを使用したテストに対応しています。AppRunコンポーネントの設計・レビュー、ルートの配線、状態フローの管理、AppRunテストの作成時に活用してください。
desloppify
コードベースのヘルスチェックと技術負債の追跡ツールです。コード品質、技術負債、デッドコード、大規模ファイル、ゴッドクラス、重複関数、コードスメル、命名規則の問題、インポートサイクル、結合度の問題についてユーザーが質問した場合に使用してください。また、ヘルススコアの確認、次の改善項目の提案、クリーンアップ計画の作成をリクエストされた際にも対応します。29言語に対応しています。
debugging-and-error-recovery
テストが失敗したり、ビルドが壊れたり、動作が期待と異なったり、予期しないエラーが発生したりした場合に、体系的な根本原因デバッグをガイドします。推測ではなく、根本原因を見つけて修正するための体系的なアプローチが必要な場合に使用してください。
test-driven-development
テスト駆動開発により実装を進めます。ロジックの実装、バグの修正、動作の変更など、あらゆる場面で活用できます。コードが正常に動作することを証明する必要がある場合、バグ報告を受けた場合、既存機能を修正する予定がある場合に使用してください。
incremental-implementation
変更を段階的に実施します。複数のファイルに影響する機能や変更を実装する場合に使用してください。大量のコードを一度に書こうとしている場合や、タスクが一度では完結できないほど大きい場合に活用します。