🐈
pytestユースケースまとめてみた
きっかけ
社内勉強会リクエストがあったのでまとめてたんですが、これ普通に忘備録として記事にしといてもいんじゃねと思って書きました。
すぐ動確したい人向け
以下参考ください。
※vscodeによるdevconainerを使用しています。1. 基本的な3Aパターンと例外テスト
基本的な3Aパターン
概要
3Aパターンは、テストコードを構造化するための基本的なパターン。
- Arrange: テストの準備
- Act: テスト対象の実行
- Assert: 結果の検証
コード例
ユースケース
- 単純な関数やメソッドのテスト
- 入力と出力の関係が明確なケース
- 基本的な機能のテスト
実行例
pytest -v test_calculator.py::TestCalculator3A
出力例
test_calculator.py::TestCalculator3A::test_add_3a_pattern PASSED
例外テスト
概要
例外が適切に発生するかをテストするパターン。
- 特定の条件で例外が発生することを確認
- 例外メッセージの内容を検証
- エラー処理の動作確認
コード例
ユースケース
- エラー処理のテスト
- 入力バリデーションのテスト
- 境界値のテスト
より高度な例外テスト
注意点
-
適切な例外タイプの指定
- あまりに広い例外(Exception)は避ける
- 具体的な例外クラスを指定する
-
メッセージの検証レベル
- 完全一致:
assert str(excinfo.value) == "expected message"
- 部分一致:
assert "expected" in str(excinfo.value)
- 完全一致:
-
コンテキストマネージャの範囲
# Good: 例外発生箇所を正確に特定
with pytest.raises(ValueError):
specific_operation()
# Bad: 範囲が広すぎる
with pytest.raises(ValueError):
setup_operation()
specific_operation()
cleanup_operation()
実行例
pytest -v test_calculator.py::TestCalculatorExceptions
出力例
test_calculator.py::TestCalculatorExceptions::test_divide_by_zero PASSED
test_calculator.py::TestCalculatorExceptions::test_invalid_input PASSED
ベストプラクティス
- 例外テストは独立したテストメソッドとして書く
- 一つのテストメソッドでは一つの例外シナリオをテスト
- 例外メッセージは必要な部分のみ検証
- テスト名は例外シナリオを明確に表現する
2. パラメトライズドテストとSetup/Teardownパターン
パラメトライズドテスト
概要
同じテストロジックを複数の入力データで実行するパターン。
- コードの重複を避ける
- 複数のテストケースを効率的に実行
- データ駆動テスト(DDT: Data-Driven Testing)の実現
基本的なパラメトライズ
複数パラメータのパラメトライズ
ユースケース
- 境界値テスト
- 異なる入力の組み合わせテスト
- 同じロジックの複数条件テスト
Setup/Teardownパターン
概要
テストの前後で必要な準備と後片付けを行うパターン。
- テストの前提条件を設定
- リソースの確保と解放
- テスト環境のクリーンアップ
基本的なSetup/Teardown
クラスレベルのSetup/Teardown
実行例とタイミング
注意点
- リソースの適切な管理
class TestResourceManagement:
def setup_method(self):
self.file = open("test.txt", "w")
def teardown_method(self):
self.file.close() # 必ずリソースを解放
- 例外処理の考慮
def setup_method(self):
try:
self.resource = expensive_setup()
except Exception as e:
cleanup_partial_setup()
raise
ベストプラクティス
- setup/teardownは最小限に
- 共有リソースは慎重に管理
- 副作用を考慮
- テストの独立性を保持
実行例
pytest -sv test_calculator.py::TestCalculatorWithSetup # -sオプションでprint出力を表示
出力例
test_calculator.py::TestCalculatorWithSetup::test_add_with_setup
Setup: Creating calculator instance
PASSED
Teardown: Cleaning up resources
3. モック/スタブ
モック/スタブ
概要
モックとスタブは、テスト対象の依存コンポーネントを置き換えるために使用する。
- 外部依存の分離
- 複雑な動作のシミュレーション
- エッジケースのテスト
基本的なモック
高度なモック例
スパイの使用
モックの使用例
- 外部APIのモック
def test_external_api(mocker):
mock_response = mocker.Mock()
mock_response.json.return_value = {"data": "mocked"}
mock_get = mocker.patch("requests.get", return_value=mock_response)
# テスト対象のコード実行
result = get_external_data()
assert result == {"data": "mocked"}
- データベース操作のモック
def test_database_operation(mocker):
mock_db = mocker.Mock()
mock_db.query.return_value = ["result1", "result2"]
# DBアクセスをモック化
mocker.patch("my_app.get_db", return_value=mock_db)
ベストプラクティス
- モックは必要最小限に
- 実装の詳細ではなくインターフェースをモック
- モックの検証は適切なレベルで
- フィクスチャとモックの組み合わせを考慮
実行例
pytest -sv test_calculator.py::TestCalculatorWithMocks
出力例
test_calculator.py::TestCalculatorWithMocks::test_add_with_mock
PASSED
4. フィクスチャの使用とスコープ付きフィクスチャ
フィクスチャの使用
概要
フィクスチャは、テストに必要なセットアップを再利用可能な形で提供する。
- テストの前提条件を整える
- リソースの共有と再利用
- 依存関係の注入
基本的なフィクスチャ
スコープ付きフィクスチャ
概要
異なるスコープでフィクスチャを共有・再利用する機能。
- function: 各テスト関数ごと(デフォルト)
- class: テストクラスごと
- module: テストモジュールごと
- session: テストセッション全体
スコープの例
自動使用フィクスチャ
スコープの組み合わせ
フィクスチャの継承と依存関係
ベストプラクティス
-
適切なスコープの選択
- セットアップコストが高い → より広いスコープ
- 独立性が必要 → 狭いスコープ
-
リソース管理
- 広いスコープ → リソースの解放に注意
- メモリ使用量を考慮
-
依存関係の管理
@pytest.fixture(scope="session")
def database():
return Database()
@pytest.fixture(scope="function")
def transaction(database):
# データベースフィクスチャに依存
txn = database.begin()
yield txn
txn.rollback()
実行例
pytest -sv test_calculator.py
出力例
Session fixture: setup
Module fixture: setup
Class fixture: setup
Function fixture: setup
Auto fixture: setup
Test execution
Auto fixture: teardown
Function fixture: teardown
Class fixture: teardown
Module fixture: teardown
Session fixture: teardown
5. スキップ
条件付きスキップ
概要
特定の条件下でテストをスキップするための機能。
- 環境依存のテストの制御
- 未実装機能のテストの一時的なスキップ
- プラットフォーム固有のテストの管理
基本的なスキップ
実践的なスキップ例
環境変数に基づくスキップ
import os
class TestEnvironmentDependent:
@pytest.mark.skipif(
not os.environ.get("DATABASE_URL"),
reason="DATABASE_URLが設定されていません"
)
def test_database_connection(self):
# データベース接続テスト
pass
@pytest.mark.skipif(
os.environ.get("CI") == "true",
reason="CIでは実行しない"
)
def test_local_only(self):
# ローカル環境でのみ実行
pass
6. その他の高度なパターン
非同期テスト
概要
非同期処理(async/await)のテストを行うためのパターン。
基本的な非同期テスト
非同期フィクスチャ
プロパティベーステスト
概要
Hypothesisを使用して、ランダムなデータでテストを行う。
コンテキストマネージャを使用したテスト
概要
リソースの確保と解放を制御するパターン。
カスタムフィクスチャファクトリー
概要
動的にフィクスチャを生成するパターン。
テストのグループ化とネスト
概要
関連するテストをグループ化して構造化するパターン。
パラメータ化されたフィクスチャ
概要
フィクスチャ自体をパラメータ化するパターン。
高度な設定とカスタマイズ
pyproject.tomlの設定
conftest.pyでのグローバル設定
カスタムマーカーの使用
グローバルマーカーの使用
実行とデバッグ
実行オプション
# 並列実行
pytest -n auto
# 失敗したテストのみ再実行
pytest --lf
# カバレッジレポート
pytest --cov=src
# HTMLレポート生成
pytest --html=report.html
# 特定のマーカーのテストのみ実行
pytest -v -m slow
pytest -v -m smoke
pytest -v -m integration
pytest -v -m custom_mark
# マーカーの組み合わせ
pytest -v -m "slow or smoke"
pytest -v -m "not slow"
デバッグ支援
def test_with_debug():
x = 1
y = 2
breakpoint()
result = x + y
assert result == 3
Discussion