Webアプリケーションの品質と開発スピードを維持するうえで、信頼性の高い「テストハーネス(テスト実行基盤・環境)」の設計は欠かせません。

特に FastAPI を用いたバックエンド開発において、データベース接続・外部API連携・認証処理などの依存関係をいかに効率よく制御するかは、テストハーネスの設計美を左右する大きなポイントです。

この記事では、テストハーネス構築という視点から FastAPI の強力な機能である dependency_overrides について解説し、堅牢なテスト基盤を作るための実践例を紹介します。

スポンサーリンク

1. テストハーネス設計における「依存関係」の課題

テストハーネスを設計・構築する際、以下のような課題に直面したことはないでしょうか?

  • 本番用DBや外部APIに誤ってリクエストが飛んでしまう
  • テストのたびに認証トークンを発行したり、本番相当のセットアップが必要で実行が遅い
  • モック化のために本番コード内に if env == "test": のような条件分岐が散らかる

優れたテストハーネスの条件は、「本番コードの純粋さを保ったまま、テスト環境側から確定的な(環境に左右されない)依存オブジェクトを安全に注入できること」です。

これを FastAPI の仕組みだけで美しく解決するのが Dependency Override(依存関係の上書き)機能です。

2. FastAPI の Dependency Override とは?

FastAPI では、普段から Depends() を使って依存関係の注入(DI: Dependency Injection)を行いますが、app.dependency_overrides を使うと、「アプリ側のコードを一切変更せずに、特定関数の呼び出しをテスト用関数へ横取りして差し替える」 ことが可能になります。

構造としてはシンプルで、FastAPI アプリケーションインスタンスが保持する辞書(Dict)に対して、「置き換え元の関数」 と 「置き換え先の関数」 のペアを登録するだけです。

スポンサーリンク

3. 実践:Dependency Override を使った基本パターン

まずはシンプルな例で動作イメージを確認してみましょう。

本番側のAPI実装 (app/main.py)

from fastapi import Depends, FastAPI

app = FastAPI()


# 本番用の依存関数(実際のDBに接続する想定)
def get_db():
    raise RuntimeError("本番DBへのアクセスが発生しました!テスト環境では接続禁止です。")


@app.get("/users/me")
def read_user_me(db=Depends(get_db)):
    return {"user_id": "real_user_123", "db_status": db}

テスト側の実装 (tests/test_api.py)

from fastapi.testclient import TestClient
from app.main import app, get_db

client = TestClient(app)


# テスト用のモック関数
def override_get_db():
    return "mock_db_session"


def test_read_user_me():
    # 1. 依存関係のオーバーライドをセット
    app.dependency_overrides[get_db] = override_get_db

    # 2. リクエストの実行
    response = client.get("/users/me")

    assert response.status_code == 200
    assert response.json() == {
        "user_id": "real_user_123",
        "db_status": "mock_db_session",
    }

    # 3. テスト終了後に必ずクリアする
    app.dependency_overrides.clear()

本番コード内にはテスト用のコードや分岐が一切入っていません。テストハーネス側が一方的に依存先を差し替えることで、安全なテストを実現しています。

4. テストハーネスとして洗練させる(pytest フィクスチャとの統合)

実際のプロジェクトでテストハーネスを設計する場合、テスト関数ごとに手動で dependency_overrides を書くのは冗長ですし、clear() の呼び忘れによるテスト間汚染(副作用)のリスクがあります。

そこで、pytest の Fixture と組み合わせてハーネス側に隠蔽するのがベストプラクティスです。

ハーネス基盤の実装 (tests/conftest.py)

import pytest
from fastapi.testclient import TestClient
from app.main import app, get_db
from app.auth import get_current_user  # 例:認証関数


@pytest.fixture
def client_with_mocks():
    """DBと認証をモック化したテストクライアントを提供するハーネスFixture"""

    # モック用関数の定義
    def mock_db():
        return {"session": "fake_in_memory_db"}

    def mock_user():
        return {"id": "test_user_001", "role": "admin"}

    # ハーネス側でオーバーライドを一括適用
    app.dependency_overrides[get_db] = mock_db
    app.dependency_overrides[get_current_user] = mock_user

    # TestClient を渡してテストを実行
    yield TestClient(app)

    # テスト終了時に後始末(自動クリーンアップ)
    app.dependency_overrides.clear()

個別のテストコード (tests/test_users.py)

def test_admin_dashboard(client_with_mocks):
    # すでにDB接続と認証がモックされた状態のクライアントで即座にテスト開始
    response = client_with_mocks.get("/admin/dashboard")

    assert response.status_code == 200

このようにセットアップを conftest.py に寄せることで、開発者はテストケースの記述(入力と検証)だけに集中できるようになります。

5. テストハーネス設計における注意点・アンチパターン

Dependency Override は非常に強力ですが、設計時に気をつけたいポイントが2つあります。

clear() の徹底

app.dependency_overrides はアプリケーションインスタンス全体のグローバルな状態です。1つのテストで上書きしたままにすると、「テスト実行順序によって結果が変わる flaky なテスト」 の原因になります。かならず try...finally や pytest の yield 構文で clear() を呼び出してください。

インスタンスレベルの依存関係(クラスの call など)

関数ではなく、クラスの呼び出し可能オブジェクト(call)などを Depends() に指定している場合、オーバーライドのキーには「実際の呼び出し対象インスタンス(またはクラス)」を指定する必要があります。

# クラスインスタンスを Depends に使っている場合
auth_checker = AuthChecker()

# 差分適用時もオブジェクト参照を一致させる
app.dependency_overrides[auth_checker] = mock_auth_checker

6. まとめ

FastAPI の dependency_overrides は、テストハーネスを設計するうえで以下の大きなメリットをもたらしてくれます。

  • 本番コードの非汚染: アプリ側にテスト用の分岐を書く必要がない
  • 高速かつ確定的なテスト: 重い外部接続や認証処理を瞬時にモックへ置換できる
  • カプセル化: pytest fixture と組み合わせることで、ハーネス側で環境構築・後始末を隠蔽できる

テスト駆動開発(TDD)や継続的インテグレーション(CI)を円滑に回すためにも、FastAPI でアプリを構築する際はぜひテストハーネスの主軸として dependency_overrides をフル活用してみてください。


この記事が気に入ったら『目黒で働く分析担当の作業メモ』ご支援をお願いします!

※OFUSEに飛びます


おすすめの記事