pytest について
ユニットテストから複雑な統合テストまで対応する、Python開発者向けのテストフレームワークです。fixture、パラメーター化、マーカー、プラグインを備え、非同期テストと並列実行もサポートします。
主な機能
- 01fixtureによる依存関係と後処理の管理
- 02パラメーター化によるデータ駆動テスト
- 03マーカーによるテストの分類と選択
- 04非同期テストと並列実行のサポート
- 05プラグインによるカバレッジやモック機能の拡張
活用シーン
- 01Python関数のユニットテスト作成
- 02FastAPI、Django、Flaskアプリのテスト
- 03非同期テストや統合テストの実行
- 04カバレッジレポート作成とマーカーによる絞り込み
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ bobmatnyc/ claude-mpm-skills --skill pytestインストール後、Claude Code で/pytestを入力して読み込まれたか確認します。
同梱ファイルmetadata.json
詳細
pytest - プロフェッショナルな Python テスト
概要
pytest は業界標準の Python テストフレームワークです。フィクスチャ、パラメーター化、マーカー、プラグイン、FastAPI、Django、Flask とのシームレスな統合など、強力な機能を備えています。ユニットテストから複雑な統合テストまで、シンプルで拡張性の高いアプローチを提供します。
主な機能:
- 依存性注入のためのフィクスチャシステム
- データ駆動型テストのためのパラメーター化
- 充実したアサーションの詳細表示(
self.assertEqualは不要) - プラグインエコシステム(pytest-cov、pytest-asyncio、pytest-mock、pytest-django)
- async/await のサポート
- pytest-xdist によるテストの並列実行
- テストの検出と整理
- 詳細な失敗レポート
インストール:
# Basic pytest
pip install pytest
# With common plugins
pip install pytest pytest-cov pytest-asyncio pytest-mock
# For FastAPI testing
pip install pytest httpx pytest-asyncio
# For Django testing
pip install pytest pytest-django
# For async databases
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
テストの実行:
# Discover and run all tests
pytest
# Verbose output
pytest -v
# Show print statements
pytest -s
# Run specific test file
pytest test_math.py
# Run specific test function
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
# Test exception raising
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
# Test approximate equality
def test_float_comparison():
assert 0.1 + 0.2 == pytest.approx(0.3)
# Test containment
def test_list_contains():
result = [1, 2, 3, 4]
assert 3 in result
assert len(result) == 4
フィクスチャ - 依存性注入
基本的なフィクスチャ
# conftest.py
import pytest
@pytest.fixture
def sample_data():
"""Provide sample data for tests."""
return {"name": "Alice", "age": 30, "email": "[email protected]"}
@pytest.fixture
def empty_list():
"""Provide an 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
フィクスチャのスコープ
import pytest
# Function scope (default) - runs for each test
@pytest.fixture(scope="function")
def user():
return {"id": 1, "name": "Alice"}
# Class scope - runs once per test class
@pytest.fixture(scope="class")
def database():
db = setup_database()
yield db
db.close()
# Module scope - runs once per test module
@pytest.fixture(scope="module")
def api_client():
client = APIClient()
yield client
client.shutdown()
# Session scope - runs once for entire test session
@pytest.fixture(scope="session")
def app_config():
return load_config()
フィクスチャのセットアップとティアダウン
import pytest
import tempfile
import shutil
@pytest.fixture
def temp_directory():
"""Create a temporary directory for test."""
temp_dir = tempfile.mkdtemp()
print(f"
Setup: Created {temp_dir}")
yield temp_dir # Provide directory to test
# Teardown: cleanup after test
shutil.rmtree(temp_dir)
print(f"
Teardown: Removed {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)
フィクスチャの依存関係
import pytest
@pytest.fixture
def database_connection():
"""Database connection."""
conn = connect_to_db()
yield conn
conn.close()
@pytest.fixture
def database_session(database_connection):
"""Database session depends on connection."""
session = create_session(database_connection)
yield session
session.rollback()
session.close()
@pytest.fixture
def user_repository(database_session):
"""User repository depends on session."""
return UserRepository(database_session)
def test_create_user(user_repository):
user = user_repository.create(name="Alice", email="[email protected]")
assert user.name == "Alice"
パラメーター化 - データ駆動型テスト
基本的なパラメーター化
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 を使ったパラメーター化
@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
間接的なパラメーター化(フィクスチャ)
@pytest.fixture
def user_data(request):
"""Create user based on parameter."""
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
# Skip test
@pytest.mark.skip(reason="Not implemented yet")
def test_future_feature():
pass
# Skip conditionally
@pytest.mark.skipif(sys.platform == "win32", reason="Unix-only test")
def test_unix_specific():
pass
# Expected failure
@pytest.mark.xfail(reason="Known bug #123")
def test_known_bug():
assert False
# Slow test marker
@pytest.mark.slow
def test_expensive_operation():
time.sleep(5)
assert True
カスタムマーカー
# pytest.ini
[pytest]
markers =
slow: marks tests as slow (deselect with '-m "not slow"')
integration: marks tests as integration tests
unit: marks tests as unit tests
smoke: marks tests as smoke tests
# 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():
# Integration test with database
pass
@pytest.mark.smoke
def test_critical_path():
# Smoke test for critical functionality
pass
マーカーを指定してテストを実行:
# Run only unit tests
pytest -m unit
# Run all except slow tests
pytest -m "not slow"
# Run integration tests
pytest -m integration
# Run unit AND integration
pytest -m "unit or integration"
# Run smoke tests only
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 test client."""
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():
"""Async test client for 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
# Test database
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():
"""Create test database."""
Base.metadata.create_all(bind=engine)
yield
Base.metadata.drop_all(bind=engine)
@pytest.fixture
def client(test_db):
"""Override database dependency."""
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": "[email protected]", "password": "secret"} # pragma: allowlist secret
)
assert response.status_code == 200
assert response.json()["email"] == "[email protected]"
def test_read_users(client):
# Create user first
client.post("/users", json={"email": "[email protected]", "password": "pass1"}) # pragma: allowlist secret
client.post("/users", json={"email": "[email protected]", "password": "pass2"}) # pragma: allowlist secret
# Read users
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="[email protected]",
name="Test User"
)
assert user.email == "[email protected]"
assert user.is_active is True
@pytest.mark.django_db
def test_user_unique_email():
User.objects.create(email="[email protected]", name="User 1")
with pytest.raises(Exception): # IntegrityError
User.objects.create(email="[email protected]", 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):
# Create test data
User.objects.create(email="[email protected]", name="User 1")
User.objects.create(email="[email protected]", name="User 2")
# Test view
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="[email protected]", name="User 1")
User.objects.create(email="[email protected]", 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": "[email protected]", "name": "New User"}
response = api_client.post("/api/users/", data)
assert response.status_code == 201
assert User.objects.filter(email="[email protected]").exists()
モックとパッチ
pytest-mock(pytest.fixture.mocker)
# Install: 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):
# Mock 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):
# Database call
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()
# Mock the get_user method
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):
# First call fails, second succeeds
mock_api = mocker.patch("requests.get")
mock_api.side_effect = [
requests.exceptions.Timeout(), # First call
mocker.Mock(json=lambda: {"status": "ok"}) # Second call
]
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")
# Call code that uses the function
module.run_workflow()
# Verify it was called
assert spy.call_count == 1
spy.assert_called_once_with(arg1="value", arg2=42)
カバレッジとレポート
pytest-cov の設定
# Install
pip install pytest-cov
# Run with coverage
pytest --cov=app --cov-report=html --cov-report=term
# Generate coverage report
pytest --cov=app --cov-report=term-missing
# Coverage with minimum threshold
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 report (opens in browser)
pytest --cov=app --cov-report=html
open htmlcov/index.html
# Terminal report with missing lines
pytest --cov=app --cov-report=term-missing
# XML report (for CI/CD)
pytest --cov=app --cov-report=xml
# JSON report
pytest --cov=app --cov-report=json
非同期テスト
pytest-asyncio
# Install: pip install pytest-asyncio
# conftest.py
import pytest
# Enable asyncio mode
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):
# Mock aiohttp response
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"
非同期フィクスチャ
@pytest.fixture
async def async_db_session():
"""Async database 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="[email protected]", name="Test")
async_db_session.add(user)
await async_db_session.commit()
result = await async_db_session.execute(
select(User).where(User.email == "[email protected]")
)
assert result.scalar_one().name == "Test"
ローカルの pytest プロファイル(各リポジトリ)
各プロジェクトの pyproject.toml でよく使われる設定:
asyncio_mode = "auto"(mcp-browser、mcp-memory、claude-mpm、edgar のデフォルト)- CI の一貫性を保つため、
addoptsに--strict-markersと--strict-configを含めます - カバレッジフラグ:
--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 # Shared fixtures
│ ├── test_models.py # Model tests
│ ├── test_services.py # Service tests
│ ├── test_api.py # API tests
│ └── integration/
│ ├── __init__.py
│ └── test_workflows.py
└── pytest.ini
2. 命名規則
# ✅ GOOD: Clear test names
def test_user_creation_with_valid_email():
pass
def test_user_creation_raises_error_for_duplicate_email():
pass
# ❌ BAD: Vague names
def test_user1():
pass
def test_case2():
pass
3. Arrange-Act-Assert パターン
def test_user_service_creates_user():
# Arrange: Setup test data and dependencies
service = UserService(database=mock_db)
user_data = {"email": "[email protected]", "name": "Test"}
# Act: Perform the action being tested
result = service.create_user(user_data)
# Assert: Verify the outcome
assert result.email == "[email protected]"
assert result.id is not None
4. 共通のセットアップにはフィクスチャを使う
# ❌ BAD: Repeated setup
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()
# ✅ GOOD: Fixture-based setup
@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. 類似したテストをパラメーター化する
# ❌ BAD: Duplicate test code
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
# ✅ GOOD: Parametrized tests
@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 つのテストでは 1 つのことをテストする
# ❌ BAD: Testing multiple things
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
# ✅ GOOD: Separate tests
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. 外部依存関係をモック化する
# ✅ GOOD: Mock external 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"
# ❌ BAD: Real API call in test
def test_fetch_user_data():
result = fetch_user_data(user_id=1) # Real HTTP request!
assert result["name"] == "Alice"
よくある落とし穴
❌ アンチパターン 1:テストが実行順序に依存する
# WRONG: Tests should be independent
class TestUserWorkflow:
user_id = None
def test_create_user(self):
user = create_user()
TestUserWorkflow.user_id = user.id
def test_update_user(self):
# Fails if test_create_user didn't run first!
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:リソースをクリーンアップしない
# WRONG: Database not cleaned up
def test_user_creation():
db = setup_database()
user = create_user(db)
assert user.id is not None
# Database connection not closed!
正しい例:
@pytest.fixture
def db():
database = setup_database()
yield database
database.close() # Cleanup
❌ アンチパターン 3:実装の詳細をテストする
# WRONG: Testing internal implementation
def test_user_service_uses_cache():
service = UserService()
service.get_user(1)
assert service._cache.has_key(1) # Testing internal cache!
正しい例:
# Test behavior, not implementation
def test_user_service_returns_user():
service = UserService()
user = service.get_user(1)
assert user.id == 1
❌ アンチパターン 4:pytest の機能を使わない
# WRONG: Using unittest assertions
import unittest
def test_addition():
result = add(2, 3)
unittest.TestCase().assertEqual(result, 5)
正しい例:
# Use pytest's rich assertions
def test_addition():
assert add(2, 3) == 5
❌ アンチパターン 5:フィクスチャが複雑すぎる
# WRONG: Fixture does too much
@pytest.fixture
def everything():
db = setup_db()
user = create_user(db)
session = login(user)
cache = setup_cache()
# ... too many things!
return {"db": db, "user": user, "session": session, "cache": cache}
正しい例:
# Separate, composable fixtures
@pytest.fixture
def db():
return setup_db()
@pytest.fixture
def user(db):
return create_user(db)
@pytest.fixture
def session(user):
return login(user)
クイックリファレンス
よく使うコマンド
# Run all tests
pytest
# Verbose output
pytest -v
# Show print statements
pytest -s
# Run specific file
pytest tests/test_api.py
# Run specific test
pytest tests/test_api.py::test_create_user
# Run by marker
pytest -m unit
pytest -m "not slow"
# Run with coverage
pytest --cov=app --cov-report=html
# Parallel execution
pytest -n auto # Requires pytest-xdist
# Stop on first failure
pytest -x
# Show local variables on failure
pytest -l
# Run last failed tests
pytest --lf
# Run failed tests first
pytest --ff
pytest.ini のテンプレート
[pytest]
# Minimum pytest version
minversion = 7.0
# Test discovery patterns
python_files = test_*.py *_test.py
python_classes = Test*
python_functions = test_*
# Test paths
testpaths = tests
# Command line options
addopts =
-v
--strict-markers
--cov=app
--cov-report=html
--cov-report=term-missing
--cov-fail-under=80
# Markers
markers =
unit: Unit tests
integration: Integration tests
slow: Slow-running tests
smoke: Smoke tests for critical paths
# Django settings (if using Django)
DJANGO_SETTINGS_MODULE = myproject.settings
# Asyncio mode
asyncio_mode = auto
conftest.py のテンプレート
# conftest.py
import pytest
from fastapi.testclient import TestClient
from app.main import app
# FastAPI client fixture
@pytest.fixture
def client():
return TestClient(app)
# Database fixture
@pytest.fixture(scope="function")
def db():
database = setup_test_database()
yield database
database.close()
# Mock user fixture
@pytest.fixture
def mock_user():
return {"id": 1, "email": "[email protected]", "name": "Test User"}
# Custom pytest configuration
def pytest_configure(config):
config.addinivalue_line("markers", "api: API tests")
config.addinivalue_line("markers", "db: Database tests")
リソース
- 公式ドキュメント: 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 開発サーバーのパターンとテストフィクスチャ
- test-driven-development: TDD ワークフロー全体(RED/GREEN/REFACTOR サイクル)
- systematic-debugging: 失敗するテストの根本原因の調査
TDD ワークフローのクイックリファレンス(単独での使用向けにインライン化)
RED → GREEN → REFACTOR サイクル:
-
RED フェーズ: 失敗するテストを書く
def test_should_authenticate_user_when_credentials_valid(): # Test that describes desired behavior user = User(username='alice', password='secret123') result = authenticate(user) assert result.is_authenticated is True # This test will fail because authenticate() doesn't exist yet -
GREEN フェーズ: テストに合格させる
def authenticate(user): # Minimum code to pass the test if user.username == 'alice' and user.password == 'secret123': # pragma: allowlist secret return AuthResult(is_authenticated=True) return AuthResult(is_authenticated=False) -
REFACTOR フェーズ: コードを改善する
def authenticate(user): # Clean up while keeping tests green 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: Set up test data
user_data = {'username': 'alice', 'email': '[email protected]'}
# Act: Perform the action
user = create_user(user_data)
# Assert: Verify outcome
assert user.username == 'alice'
assert user.email == '[email protected]'
デバッグのクイックリファレンス(単独での使用向けにインライン化)
フェーズ 1: 根本原因の調査
- エラーメッセージ(スタックトレース、行番号)を最後まで読む
- 一貫して再現させる(正確な手順を記録する)
- 最近の変更を確認する(git log、git diff)
- 何が変わったのか、なぜそれが失敗の原因になり得るのかを把握する
フェーズ 2: 問題を切り分ける
# Use pytest's built-in debugging
pytest tests/test_auth.py -vv --pdb # Drop into debugger on failure
pytest tests/test_auth.py -x # Stop on first failure
pytest tests/test_auth.py -k "auth" # Run only auth-related tests
# Add strategic print/logging
def test_complex_workflow():
user = create_user({'username': 'test'})
print(f"DEBUG: Created user {user.id}") # Visible with pytest -s
result = process_user(user)
print(f"DEBUG: Result status {result.status}")
assert result.success
フェーズ 3: 根本原因を修正する
- 症状ではなく、根本的な問題を修正する
- 再発防止のために回帰テストを追加する
- 修正によってほかのテストが壊れないことを確認する
フェーズ 4: 解決策を検証する
# Run full test suite
pytest
# Run with coverage
pytest --cov=src --cov-report=html
# Verify specific test patterns
pytest -k "auth or login" -v
[TDD とデバッグのワークフロー全体は、各スキルを一緒に導入すると利用できます]
Python のコード品質に関するアンチパターン
テスト対象のコードがクリーンであれば、テストも容易になります。また、Python の品質に関するいくつかの欠陥は、不安定なテストや、問題を検出せずに通過するテストを直接引き起こします(過度に広い except、不適切な例外クラス、同一性と等価性の取り違えなど)。コード品質に関するアンチパターンは、テストスキルではなく、専用のスキルで扱うようになりました。
Python で特に重要なアンチパターン(例外階層の正しさ、シングルトンの同一性比較、例外処理の範囲を絞ること、ワイルドカードインポートの回避、マジックナンバーへの名前付け、未使用のローカル変数の削除)と、準拠していない例と準拠している例、それぞれのテスト方法については、python-code-quality スキル(toolchains/python/quality/code-quality)をご覧ください。プロジェクト全体を対象とした、重大度タグ付きのレビューチェックリストについては、code-review-standards スキルをご覧ください。
pytest のバージョン互換性: このスキルは pytest 7.0 以降を対象としており、2025 年時点の Python テストにおける最新のベストプラクティスを反映しています。