🐈

pytestユースケースまとめてみた

2025/01/26に公開

きっかけ

社内勉強会リクエストがあったのでまとめてたんですが、これ普通に忘備録として記事にしといてもいんじゃねと思って書きました。

すぐ動確したい人向け

以下参考ください。
https://github.com/akki-F/pytest-handson.git
※vscodeによるdevconainerを使用しています。

1. 基本的な3Aパターンと例外テスト

基本的な3Aパターン

概要

3Aパターンは、テストコードを構造化するための基本的なパターン。

  • Arrange: テストの準備
  • Act: テスト対象の実行
  • Assert: 結果の検証

コード例

https://github.com/akki-F/pytest-handson/blob/main/handson/tests/test_calculator.py#L12-L24

ユースケース

  • 単純な関数やメソッドのテスト
  • 入力と出力の関係が明確なケース
  • 基本的な機能のテスト

実行例

pytest -v test_calculator.py::TestCalculator3A

出力例

test_calculator.py::TestCalculator3A::test_add_3a_pattern PASSED

例外テスト

概要

例外が適切に発生するかをテストするパターン。

  • 特定の条件で例外が発生することを確認
  • 例外メッセージの内容を検証
  • エラー処理の動作確認

コード例

https://github.com/akki-F/pytest-handson/blob/main/handson/tests/test_calculator.py#L27-L39

ユースケース

  • エラー処理のテスト
  • 入力バリデーションのテスト
  • 境界値のテスト

より高度な例外テスト

https://github.com/akki-F/pytest-handson/blob/main/handson/tests/test_calculator.py#L41-L59

注意点

  1. 適切な例外タイプの指定

    • あまりに広い例外(Exception)は避ける
    • 具体的な例外クラスを指定する
  2. メッセージの検証レベル

    • 完全一致: assert str(excinfo.value) == "expected message"
    • 部分一致: assert "expected" in str(excinfo.value)
  3. コンテキストマネージャの範囲

# 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

ベストプラクティス

  1. 例外テストは独立したテストメソッドとして書く
  2. 一つのテストメソッドでは一つの例外シナリオをテスト
  3. 例外メッセージは必要な部分のみ検証
  4. テスト名は例外シナリオを明確に表現する

2. パラメトライズドテストとSetup/Teardownパターン

パラメトライズドテスト

概要

同じテストロジックを複数の入力データで実行するパターン。

  • コードの重複を避ける
  • 複数のテストケースを効率的に実行
  • データ駆動テスト(DDT: Data-Driven Testing)の実現

基本的なパラメトライズ

https://github.com/akki-F/pytest-handson/blob/main/handson/tests/test_calculator.py#L63-L76

複数パラメータのパラメトライズ

https://github.com/akki-F/pytest-handson/blob/main/handson/tests/test_calculator.py#L79-L85

ユースケース

  • 境界値テスト
  • 異なる入力の組み合わせテスト
  • 同じロジックの複数条件テスト

Setup/Teardownパターン

概要

テストの前後で必要な準備と後片付けを行うパターン。

  • テストの前提条件を設定
  • リソースの確保と解放
  • テスト環境のクリーンアップ

基本的なSetup/Teardown

https://github.com/akki-F/pytest-handson/blob/main/handson/tests/test_calculator.py#L88-L107

クラスレベルのSetup/Teardown

https://github.com/akki-F/pytest-handson/blob/main/handson/tests/test_calculator.py#L110-L130

実行例とタイミング

https://github.com/akki-F/pytest-handson/blob/main/handson/tests/test_calculator.py#L133-L150

注意点

  1. リソースの適切な管理
class TestResourceManagement:
    def setup_method(self):
        self.file = open("test.txt", "w")

    def teardown_method(self):
        self.file.close()  # 必ずリソースを解放
  1. 例外処理の考慮
def setup_method(self):
    try:
        self.resource = expensive_setup()
    except Exception as e:
        cleanup_partial_setup()
        raise

ベストプラクティス

  1. setup/teardownは最小限に
  2. 共有リソースは慎重に管理
  3. 副作用を考慮
  4. テストの独立性を保持

実行例

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. モック/スタブ

モック/スタブ

概要

モックとスタブは、テスト対象の依存コンポーネントを置き換えるために使用する。

  • 外部依存の分離
  • 複雑な動作のシミュレーション
  • エッジケースのテスト

基本的なモック

https://github.com/akki-F/pytest-handson/blob/main/handson/tests/test_calculator.py#L154-L163

高度なモック例

https://github.com/akki-F/pytest-handson/blob/main/handson/tests/test_calculator.py#L166-L189

スパイの使用

https://github.com/akki-F/pytest-handson/blob/main/handson/tests/test_calculator.py#L192-L201

モックの使用例

  1. 外部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"}
  1. データベース操作のモック
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)

ベストプラクティス

  1. モックは必要最小限に
  2. 実装の詳細ではなくインターフェースをモック
  3. モックの検証は適切なレベルで
  4. フィクスチャとモックの組み合わせを考慮

実行例

pytest -sv test_calculator.py::TestCalculatorWithMocks

出力例

test_calculator.py::TestCalculatorWithMocks::test_add_with_mock 
PASSED

4. フィクスチャの使用とスコープ付きフィクスチャ

フィクスチャの使用

概要

フィクスチャは、テストに必要なセットアップを再利用可能な形で提供する。

  • テストの前提条件を整える
  • リソースの共有と再利用
  • 依存関係の注入

基本的なフィクスチャ

https://github.com/akki-F/pytest-handson/blob/main/handson/tests/test_calculator.py#L204-L220

スコープ付きフィクスチャ

概要

異なるスコープでフィクスチャを共有・再利用する機能。

  • function: 各テスト関数ごと(デフォルト)
  • class: テストクラスごと
  • module: テストモジュールごと
  • session: テストセッション全体

スコープの例

https://github.com/akki-F/pytest-handson/blob/main/handson/tests/test_calculator.py#L223-L256

自動使用フィクスチャ

https://github.com/akki-F/pytest-handson/blob/main/handson/tests/test_calculator.py#L259-L264

スコープの組み合わせ

https://github.com/akki-F/pytest-handson/blob/main/handson/tests/test_calculator.py#L267-L275

フィクスチャの継承と依存関係

https://github.com/akki-F/pytest-handson/blob/main/handson/tests/test_calculator.py#L278-L290

ベストプラクティス

  1. 適切なスコープの選択

    • セットアップコストが高い → より広いスコープ
    • 独立性が必要 → 狭いスコープ
  2. リソース管理

    • 広いスコープ → リソースの解放に注意
    • メモリ使用量を考慮
  3. 依存関係の管理

@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. スキップ

条件付きスキップ

概要

特定の条件下でテストをスキップするための機能。

  • 環境依存のテストの制御
  • 未実装機能のテストの一時的なスキップ
  • プラットフォーム固有のテストの管理

基本的なスキップ

https://github.com/akki-F/pytest-handson/blob/main/handson/tests/test_calculator.py#L293-L301

実践的なスキップ例

https://github.com/akki-F/pytest-handson/blob/main/handson/tests/test_calculator.py#L304-L321

環境変数に基づくスキップ

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)のテストを行うためのパターン。

基本的な非同期テスト

https://github.com/akki-F/pytest-handson/blob/main/handson/tests/test_calculator.py#L325-L333

非同期フィクスチャ

https://github.com/akki-F/pytest-handson/blob/main/handson/tests/test_calculator.py#L336-L348

プロパティベーステスト

概要

Hypothesisを使用して、ランダムなデータでテストを行う。
https://github.com/akki-F/pytest-handson/blob/main/handson/tests/test_calculator.py#L365-L375

コンテキストマネージャを使用したテスト

概要

リソースの確保と解放を制御するパターン。

https://github.com/akki-F/pytest-handson/blob/main/handson/tests/test_calculator.py#L378-L394

カスタムフィクスチャファクトリー

概要

動的にフィクスチャを生成するパターン。

https://github.com/akki-F/pytest-handson/blob/main/handson/tests/test_calculator.py#L397-L409

テストのグループ化とネスト

概要

関連するテストをグループ化して構造化するパターン。

https://github.com/akki-F/pytest-handson/blob/main/handson/tests/test_calculator.py#L412-L419

パラメータ化されたフィクスチャ

概要

フィクスチャ自体をパラメータ化するパターン。

https://github.com/akki-F/pytest-handson/blob/main/handson/tests/test_calculator.py#L422-L429

高度な設定とカスタマイズ

pyproject.tomlの設定

https://github.com/akki-F/pytest-handson/blob/main/pyproject.toml#L1-L7

conftest.pyでのグローバル設定

https://github.com/akki-F/pytest-handson/blob/main/handson/tests/conftest.py#L1-L10

カスタムマーカーの使用

https://github.com/akki-F/pytest-handson/blob/main/handson/tests/test_calculator.py#L432-L445

グローバルマーカーの使用

https://github.com/akki-F/pytest-handson/blob/main/handson/tests/test_calculator.py#L362-L363

実行とデバッグ

実行オプション

# 並列実行
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