【第3回】Python pytest 入門 — モック・パッチでDBや外部APIへの依存を切り離す

プログラミング

この記事でわかること

  • モックが必要な理由と基本的な考え方
  • unittest.mock.patch の使い方とよくある落とし穴
  • pytest-mock で書くすっきりしたモックテスト
  • FastAPIの dependency_overrides を使ったDI層のモック差し替え
  • 非同期処理で AsyncMock を使う方法
  • どの場面でどのモック方法を選ぶか

前回のおさらいと今回のテーマ

前回は conftest.pyfixture の使い方を中心に、FastAPIアプリに対してテストを書く土台を整えました。TestClient でエンドポイントを叩いてレスポンスを検証する、という流れはだいぶ慣れてきたと思います。

ただ、実際のアプリを書いていると「DBに接続している」「外部のAPIを叩いている」という処理がどうしても混じってきます。そういう処理を含んだまま全部のテストを走らせると、テストが遅くなるし、ネットワーク状況や本番データに結果が左右されてしまう。これを解決するのが モック(mock) です。

今回は unittest.mockpytest-mock の使い方、FastAPIの dependency_overrides を使ったDIレイヤーでのモック差し替えを順番に見ていきます。

モックとは何か、なぜ必要か

モックとは、本物のオブジェクトの代わりにテスト用の「偽物」を差し込む仕組みです。外部APIへのHTTPリクエストをモックすれば、実際にネットワークに繋がなくても「このAPIが200を返したとき、アプリはどう振る舞うか」を確認できます。

DynamoDBやRDSへの接続も同じで、本物のDBが必要なテストはCI/CD環境のセットアップが面倒になりますし、実行速度も遅くなりがちです。ユニットテストの段階ではモックで切り離して、速く・安定して動かすのが基本的な考え方です。

余談ですが、自分が最初にモックを使い始めたきっかけは「テスト中に本番DynamoDBを叩いて思いがけず課金が発生した」という体験で、それ以来モックには感謝しています。

まず unittest.mock の基本を押さえる

Python標準ライブラリの unittest.mock を使う方法から見ていきます。patch を使って、テスト中だけ特定の関数やクラスを差し替えるのが基本パターンです。

こんなシンプルな例で確認します。外部の天気APIを叩く関数があるとします。

# app/weather.py
import httpx

def get_weather(city: str) -> dict:
    response = httpx.get(f"https://api.example.com/weather?city={city}")
    response.raise_for_status()
    return response.json()

この関数をテストするとき、実際にHTTPリクエストを飛ばしたくないので patch で差し替えます。

# tests/test_weather.py
from unittest.mock import patch, MagicMock
from app.weather import get_weather

def test_get_weather_success():
    mock_response = MagicMock()
    mock_response.json.return_value = {"city": "Tokyo", "temp": 25}
    mock_response.raise_for_status.return_value = None

    with patch("app.weather.httpx.get", return_value=mock_response):
        result = get_weather("Tokyo")

    assert result["city"] == "Tokyo"
    assert result["temp"] == 25

ポイントは patch に渡すパスです。"httpx.get" ではなく "app.weather.httpx.get" と、モック対象が使われている場所(モジュールパス)を指定するのが鉄則です。ここを間違えると patch が効かないという罠にはまります。自分も最初はここで30分溶かしました。

pytest-mock を使うともう少し楽に書ける

pytest-mock をインストールすると、mocker というフィクスチャが使えるようになります。unittest.mock の仕組みを便利に扱えるようにしたもの、と考えるとイメージしやすいです。コンテキストマネージャーを使わなくていい分、テストコードがすっきりします。

pip install pytest-mock
# tests/test_weather.py(pytest-mock版)
from app.weather import get_weather

def test_get_weather_success(mocker):
    mock_response = mocker.MagicMock()
    mock_response.json.return_value = {"city": "Tokyo", "temp": 25}
    mock_response.raise_for_status.return_value = None

    mocker.patch("app.weather.httpx.get", return_value=mock_response)

    result = get_weather("Tokyo")
    assert result["temp"] == 25

書き方はほぼ同じですが、with patch(...) as mock: のネストが不要になります。複数のパッチを当てるときにネストが深くなりがちな unittest.mock と比べると、mocker.patch を並べるだけで済むのは地味に助かります。

return_value と side_effect

モックの設定でよく使う2つのオプションを整理しておきます。

  • return_value:呼び出したときに返す値を固定する
  • side_effect:例外を発生させたり、呼び出しごとに違う値を返したりする
def test_get_weather_timeout(mocker):
    # タイムアウト例外が発生したときの挙動をテスト
    mocker.patch(
        "app.weather.httpx.get",
        side_effect=httpx.TimeoutException("timeout")
    )

    with pytest.raises(httpx.TimeoutException):
        get_weather("Tokyo")

エラーケースのテストは実際の環境で再現するのが難しいので、side_effect で強制的に例外を起こせるのは便利です。

FastAPIの dependency_overrides でDI層を差し替える

patch でモックを当てる方法はシンプルで使いやすいですが、FastAPIには dependency_overrides という、依存関係(Depends)ごとテスト用の実装に差し替える仕組みがあります。これを使うとよりクリーンに書けます。

たとえば、DynamoDBクライアントを返す依存関係があるとします。

# app/dependencies.py
import boto3

def get_dynamodb():
    return boto3.resource("dynamodb", region_name="ap-northeast-1")
# app/main.py
from fastapi import FastAPI, Depends
from app.dependencies import get_dynamodb

app = FastAPI()

@app.get("/items/{item_id}")
def get_item(item_id: str, db=Depends(get_dynamodb)):
    table = db.Table("items")
    response = table.get_item(Key={"item_id": item_id})
    return response.get("Item", {})

テストでは app.dependency_overrides を使ってDynamoDBクライアントをモックに差し替えます。

# tests/test_main.py
import pytest
from fastapi.testclient import TestClient
from unittest.mock import MagicMock
from app.main import app
from app.dependencies import get_dynamodb

@pytest.fixture
def mock_db():
    mock = MagicMock()
    mock.Table.return_value.get_item.return_value = {
        "Item": {"item_id": "abc", "name": "テストアイテム"}
    }
    return mock

@pytest.fixture
def client(mock_db):
    app.dependency_overrides[get_dynamodb] = lambda: mock_db
    yield TestClient(app)
    app.dependency_overrides.clear()  # テスト後にリセットしておくのが安全です

def test_get_item(client):
    response = client.get("/items/abc")
    assert response.status_code == 200
    assert response.json()["name"] == "テストアイテム"

dependency_overrides.clear() の呼び出しを忘れると、他のテストにもオーバーライドが残ってしまうことがあります。yield でフィクスチャを書いてテスト後にクリアするのがお作法です。

patch を使う方法と比べたとき、dependency_overrides の利点は「FastAPIのDIの仕組みそのものを使ってテストしている」ところです。実装に直接触らずに差し替えられるので、テストコードが実装の内部構造に依存しにくくなります。個人的には「外からきれいに差し替えられる感じ」がして好きです。

非同期処理のモック:AsyncMock

FastAPIの非同期エンドポイント(async def)をテストするとき、モック対象も非同期関数の場合は AsyncMock を使うのが一般的です。Python 3.8以降で追加された AsyncMock を使うと、非同期関数のモックが書きやすくなります。

# app/external.py
import httpx

async def fetch_user(user_id: str) -> dict:
    async with httpx.AsyncClient() as client:
        response = await client.get(f"https://api.example.com/users/{user_id}")
        return response.json()
# tests/test_external.py
import pytest
from app.external import fetch_user

@pytest.mark.asyncio
async def test_fetch_user(mocker):
    mock_response = mocker.MagicMock()
    mock_response.json.return_value = {"id": "u1", "name": "ニャンチュー"}

    mock_client = mocker.AsyncMock()
    mock_client.__aenter__.return_value.get = mocker.AsyncMock(return_value=mock_response)

    mocker.patch("app.external.httpx.AsyncClient", return_value=mock_client)

    result = await fetch_user("u1")
    assert result["name"] == "ニャンチュー"

コンテキストマネージャー(async with)を使ったクライアントのモックは少しわかりにくいです。正直ここはまだ毎回調べながら書いています。__aenter____aexit__ を意識しないといけないので、慣れるまでは「動いた!」で済ませてしまう自分がいます。

どの方法を使えばいいのか

状況によって使い分けるのが現実的だと思っています。ざっくりまとめると:

  • 関数・メソッド単位でピンポイントに差し替えたい → mocker.patch / unittest.mock.patch
  • FastAPIのDependsで注入しているもの(DB接続、認証など)を差し替えたい → dependency_overrides
  • 非同期関数を差し替えたい → AsyncMock

「モックを使いすぎると実際の動作と乖離する」という意見もあって、それはそうだなと思います。ユニットテストはモックで高速に回して、本物のDBや外部サービスとの結合テストは別で用意する、という切り分けが理想な気がします。ただ、全部きれいに分離できるかどうかはプロジェクトの規模次第で……そのあたりは統合テストの話題で触れることが多いです。

※この記事にはプロモーションが含まれます

ちなみに、お名前.com レンタルサーバー(WordPressに特化した高速レンタルサーバー。月額990円〜、独自ドメイン実質0円)も気になっています。お名前.com レンタルサーバー

まとめ

pytestでモックを扱う3つの方法をおさらいします:

  • unittest.mock.patch:標準ライブラリで、モジュールパスの指定が鉄則。複数パッチ時はネストが深くなりやすい
  • pytest-mockmocker フィクスチャで patch をすっきり書ける。日常的なモック処理はこれで十分
  • dependency_overrides:FastAPIのDI仕組みを使った差し替え。実装に依存しないテストが書ける

非同期関数には AsyncMock を使い、エラーケーステストは side_effect で例外を強制的に発生させる。どれを選ぶかは「何を差し替えたいのか」で自然と決まってきます。

テストを速く・安定して動かすために、モックはほぼ必須の道具です。最初は「パッチって何だ?」ってなりますが、何度か使ってみるとコツが掴めてくると思います。

📚 シリーズ「Python pytest 入門 — FastAPIアプリを題材にテスト設計をゼロから学ぶ」(第3回 / 全4回)

← 前回の記事: 前回の記事はこちら

→ 次回の記事: 公開後にリンクが追加されます

参考になったらクリックしてもらえると嬉しいです!

Blogmura ProgrammingProgramming Ranking
タイトルとURLをコピーしました