pytestにおけるMockの概要と実践
はじめに
テストコードの最終的な価値は 速度・再現性・保守性 に帰着する。しかし実サービスでは外部 API、メッセージキュー、DB、システム時刻など“副作用を伴う依存先”が必ず存在し、これらを直接叩くとテストは遅く、不安定で、CI でも失敗しやすい。そこで登場するのが Mock だ。本稿では Python 標準 unittest.mock
と pytest 固有の monkeypatch
を軸として深掘りする。
実装詳細が見透かせるテストは魅力的だが、安易に使うと結合度が高まりリファクタ耐性が下がる。以下の内容は「Mock の威力と落とし穴」を両輪で捉えるためのガイドである。
1. unittest.mock.MagicMock
― 動的スタブ生成器
1‑1. コア概念
MagicMock
は任意属性へのアクセス・呼び出しをフックし、呼び出し履歴 (call args list) を自動記録 する動的オブジェクトだ。生成直後は“何も設定されていないが何でも返せる”シュレディンガーモックであり、属性アクセスはすべて新たな MagicMock
を生成して返却する。
from unittest.mock import MagicMock
mock = MagicMock()
mock.foo.bar.baz() # 存在しない多段メソッドもエラーにならない
print(mock.method_calls) # [call.foo.bar.baz()]
1‑2. 主要 API
API | 説明 & 典型パターン |
---|---|
return_value |
mock.func.return_value = 123 で固定返り値。 |
side_effect |
1) 例外を投げる mock.func.side_effect = ValueError , 2) 逐次返却 = ["a","b"] , 3) ラムダで動的返却。 |
assert_called_once_with |
引数も合わせて1回呼ばれたか検証。 |
reset_mock() |
呼び出し履歴をリセットし再利用可能に。 |
1‑3. テスト設計例
def download(client, url, retries=3):
for _ in range(retries):
try:
return client.get(url).json()
except TimeoutError:
continue
raise RuntimeError("retry exceeded")
def test_download_retry():
client = MagicMock()
# 1‑2 回目はタイムアウト, 3 回目で成功
client.get.side_effect = [TimeoutError, TimeoutError, MagicMock(json=lambda: {"ok":True})]
assert download(client, "http://example") == {"ok":True}
assert client.get.call_count == 3
副作用をシーケンスで定義することで「1 回目失敗 → 2 回目成功」などの複雑シナリオを 1 行で再現できる。
2. unittest.mock.patch
― 既存参照の差し替え
2‑1. インポート境界と名前解決
patch(target, ...)
の target は "実際にコードが参照する名前空間" を指定する。したがって from module import func
と書かれていれば、差し替えるのは module.func
ではなく 呼び出し側モジュール内の func
名前 である。
# app/service.py
from vendor.client import send # ★ send のコピーがここに作られる
def submit(payload):
return send("/api", json=payload)
# test_service.py
@patch("app.service.send") # ← 呼び出し側の名前空間を指定
エンジニアがハマりやすいポイントなので、リネームを伴う import は常に意識すること。
2‑2. 高度な使い方
-
コンテキストマネージャ: 複数パッチを
with
ネストよりpatch.multiple()
にまとめ省略化。 -
autospec:
@patch("pkg.mod.Class", autospec=True)
により実際のシグネチャをコピーしたモックを生成し、呼び出し引数ミスを即時検出。 -
new_callable
: 生成済みクラスではなく Callable を渡し、パッチ時に戻り値をインスタンス化させる。
2‑3. 実践例: 非決定抽選関数を固定
# utils/random_pick.py
import secrets
def pick(items):
return secrets.choice(items)
# test_random_pick.py
from unittest.mock import patch
@patch("utils.random_pick.secrets.choice", return_value="orange")
def test_pick(_):
assert pick(["apple","orange"]) == "orange"
CI で乱数系が安定しない場合、patch 一発で決定論的にできる。
3. pytest.monkeypatch
― 軽量フィクスチャアプローチ
3‑1. なぜ monkeypatch か
MagicMock
/patch
は強力だが import 解決の手間と可読性 がデメリット。pytest の monkeypatch
は fixture 注入でスコープを自動管理しつつ、環境変数・パス・dict など“モックというより単純な値差し替え”に最適化されている。
3‑2. API 一覧
メソッド | 主用途 | 備考 |
---|---|---|
setattr(target, name, value, raising=True) |
属性差し替え | raising=False で存在しなくても OK |
setitem(mapping, key, value) |
dict 差し替え |
os.environ など mutable obj に◎ |
setenv(key, value, prepend=False) / delenv
|
環境変数 | prepend=True でパス追加 |
chdir(path) |
CWD 変更 |
tmp_path と組合せて安全 |
3‑3. 実践例: CLI ツールの Config 注入
# cli/config.py
import os, json, pathlib
CONF = json.loads(pathlib.Path(os.getenv("APP_CONF", "~/conf.json")).expanduser().read_text())
# test_cli.py
def test_default_conf(monkeypatch, tmp_path, snapshot):
# tmp に一時 config を生成
cfg = tmp_path / "conf.json"
cfg.write_text('{"mode":"test"}')
monkeypatch.setenv("APP_CONF", str(cfg))
from cli import config; import importlib; importlib.reload(config)
assert config.CONF == {"mode":"test"}
snapshot.assert_match(config.CONF) # pytest‑snapshot 併用
monkeypatch
は reload と組み合わせることで、初期化時に読み込まれるモジュール定数も安全に書き換えられる。
4. ベストプラクティスとアンチパターン
- Arrange‑Act‑Assert の分離: Mock 設定(Arrange)を fixture 化し、テスト本体はビジネスロジック(Act)と結果検証(Assert)だけに。読みやすさ向上と重複削減。
- 黒箱テスト優先: 内部 private メソッド呼び出し数より、公共 API 出力 を検証。実装に依存しすぎるとリファクタで壊れやすい。
- オーバーモック禁止: DB も API も全モック化すると、「SQL が壊れても気付かない」テストになる。ユニット < 統合 < E2E のピラミッドを意識し、“どこまで嘘をつくか” をレイヤで線引き。
- autospec の活用: 引数違いを検知できるため、将来の API 変更でもテストが赤くなる=検知できる。
-
性能 vs 現実性: 非同期コードは
AsyncMock
を使いawait
互換に。HTTPx/Pydantic/Eager evaluation など重い処理はレンジに応じて局所モック。
5. まとめ
-
MagicMock
は 動的に作れるスタブ、複雑な呼び出しシナリオ再現に強い。 -
patch
は “import された名前” を差し替え、標準ライブラリやサードパーティ依存を無痛で置換。 -
monkeypatch
は pytest 流儀で 値の上書きを簡潔に 行い、環境変数・CWD 操作に無敵。
Mock は刀。切れ味抜群だが振り回すと味方を斬る。外部依存を嘘にする範囲を最小化し、テストピラミッド全体で品質を担保しよう。