この記事でわかること
- モックが必要な理由と基本的な考え方
unittest.mock.patchの使い方とよくある落とし穴pytest-mockで書くすっきりしたモックテスト- FastAPIの
dependency_overridesを使ったDI層のモック差し替え - 非同期処理で
AsyncMockを使う方法 - どの場面でどのモック方法を選ぶか
前回のおさらいと今回のテーマ
前回は conftest.py と fixture の使い方を中心に、FastAPIアプリに対してテストを書く土台を整えました。TestClient でエンドポイントを叩いてレスポンスを検証する、という流れはだいぶ慣れてきたと思います。
ただ、実際のアプリを書いていると「DBに接続している」「外部のAPIを叩いている」という処理がどうしても混じってきます。そういう処理を含んだまま全部のテストを走らせると、テストが遅くなるし、ネットワーク状況や本番データに結果が左右されてしまう。これを解決するのが モック(mock) です。
今回は unittest.mock と pytest-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-mock:mockerフィクスチャでpatchをすっきり書ける。日常的なモック処理はこれで十分dependency_overrides:FastAPIのDI仕組みを使った差し替え。実装に依存しないテストが書ける
非同期関数には AsyncMock を使い、エラーケーステストは side_effect で例外を強制的に発生させる。どれを選ぶかは「何を差し替えたいのか」で自然と決まってきます。
テストを速く・安定して動かすために、モックはほぼ必須の道具です。最初は「パッチって何だ?」ってなりますが、何度か使ってみるとコツが掴めてくると思います。
📚 シリーズ「Python pytest 入門 — FastAPIアプリを題材にテスト設計をゼロから学ぶ」(第3回 / 全4回)
← 前回の記事: 前回の記事はこちら
→ 次回の記事: 公開後にリンクが追加されます
