| name | pytest-testing |
| description | pytest を使った Python テストの包括的な実装ガイド。設定(pyproject.toml, conftest.py)、テストの書き方、Fixtures(scope, yield, autouse)、パラメータ化、モック(unittest.mock, pytest-mock, monkeypatch)、FastAPIテスト(TestClient, httpx.AsyncClient)、カバレッジ、マーカーをカバー。pytestでのテスト実装時に使用。 |
pytest テスト実装ガイド
ワークフロー
- テスト対象の確認 - 対象コードの構造・依存関係を把握
- テストファイル作成 -
tests/ 配下に test_*.py を配置
- Fixture 設計 - 共通のセットアップを Fixture として抽出
- テスト実装 - Arrange-Act-Assert パターンで記述
- カバレッジ確認 -
pytest --cov で網羅率を確認
- リファクタリング - 重複排除、パラメータ化の適用
pytest 設定
pyproject.toml
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_functions = ["test_*"]
addopts = "-v --strict-markers --tb=short"
markers = [
"slow: 実行に時間がかかるテスト",
"integration: 結合テスト",
]
filterwarnings = ["error", "ignore::DeprecationWarning"]
[tool.coverage.run]
source = ["src"]
omit = ["*/tests/*", "*/migrations/*"]
[tool.coverage.report]
fail_under = 80
show_missing = true
exclude_lines = ["pragma: no cover", "if TYPE_CHECKING:"]
conftest.py の配置
project/
tests/
conftest.py # プロジェクト共通 Fixture
unit/
conftest.py # ユニットテスト共通
test_service.py
integration/
conftest.py # 結合テスト共通
test_api.py
テストの書き方
基本構造(AAA パターン)
def test_create_user_with_valid_data():
user_data = {"name": "Taro", "email": "taro@example.com"}
user = create_user(**user_data)
assert user.name == "Taro"
assert user.email == "taro@example.com"
クラスによるグルーピングと例外テスト
class TestUserService:
def test_create_returns_user(self):
result = UserService().create(name="Taro")
assert isinstance(result, User)
def test_create_raises_on_duplicate(self):
with pytest.raises(DuplicateError, match="既に存在"):
UserService().create(name="existing")
def test_validation_error_message():
with pytest.raises(ValueError, match=r"invalid.*email"):
validate_email("not-an-email")
Fixtures
基本と scope
@pytest.fixture
def user():
return User(name="Taro", email="taro@example.com")
@pytest.fixture(scope="module")
def db_connection():
conn = create_connection()
yield conn
conn.close()
@pytest.fixture(scope="session")
def app_config():
return load_config("test")
scope: function < class < module < package < session
yield Fixture(setup/teardown)
@pytest.fixture
def tmp_database(tmp_path):
db = Database(tmp_path / "test.db")
db.create_tables()
yield db
db.drop_tables()
db.close()
autouse と conftest.py での共有
@pytest.fixture(autouse=True)
def reset_cache():
cache.clear()
yield
cache.clear()
@pytest.fixture
def api_client():
return TestClient(app)
@pytest.fixture
def auth_headers(test_user):
token = create_token(test_user.id)
return {"Authorization": f"Bearer {token}"}
詳細パターンは references/fixture-patterns.md を参照。
パラメータ化テスト
@pytest.mark.parametrize("input_val,expected", [
("hello", "HELLO"),
("world", "WORLD"),
("", ""),
])
def test_to_upper(input_val, expected):
assert to_upper(input_val) == expected
@pytest.mark.parametrize("x", [1, 2])
@pytest.mark.parametrize("y", [10, 20])
def test_multiply(x, y):
assert multiply(x, y) == x * y
モック
unittest.mock
from unittest.mock import patch, MagicMock
@patch("app.service.send_email")
def test_register_sends_email(mock_send):
register_user("taro@example.com")
mock_send.assert_called_once_with("taro@example.com", subject="Welcome")
@patch("app.service.ExternalAPI")
def test_fetch_data(MockAPI):
MockAPI.return_value.get.return_value = {"key": "value"}
result = fetch_data()
assert result == {"key": "value"}
pytest-mock(mocker Fixture)
def test_service_calls_repository(mocker):
mock_repo = mocker.patch("app.service.UserRepository")
mock_repo.return_value.find.return_value = User(name="Taro")
result = UserService().get_user(1)
assert result.name == "Taro"
mock_repo.return_value.find.assert_called_once_with(1)
monkeypatch
def test_with_env_var(monkeypatch):
monkeypatch.setenv("API_KEY", "test-key-123")
assert get_api_key() == "test-key-123"
def test_with_patched_attr(monkeypatch):
monkeypatch.setattr("app.config.DEBUG", True)
assert is_debug_mode() is True
def test_disable_network(monkeypatch):
monkeypatch.setattr("requests.get", lambda *a, **k: (_ for _ in ()).throw(ConnectionError))
FastAPI テスト
同期テスト(TestClient)
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
def test_read_root():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello"}
def test_create_item():
response = client.post("/items", json={"name": "Item1", "price": 100})
assert response.status_code == 201
非同期テスト(httpx.AsyncClient)
import pytest
from httpx import AsyncClient, ASGITransport
from app.main import app
@pytest.mark.anyio
async def test_async_root():
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
response = await ac.get("/")
assert response.status_code == 200
詳細パターンは references/api-testing.md を参照。
カバレッジ設定(pytest-cov)
pytest --cov=src --cov-report=term-missing
pytest --cov=src --cov-report=html
pytest --cov=src --cov-fail-under=80
マーカーとテスト分類
@pytest.mark.slow
def test_heavy_computation():
assert compute_large_dataset() is not None
@pytest.mark.skip(reason="外部APIが未準備")
def test_external_api(): ...
@pytest.mark.skipif(sys.platform == "win32", reason="Linux専用")
def test_linux_only(): ...
@pytest.mark.xfail(reason="既知のバグ #123")
def test_known_bug():
assert buggy_function() == "expected"
pytest -m "not slow"
pytest -m "integration"
pytest -k "test_user and not test_user_delete"
レビューチェックリスト
リファレンス