1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

#0141(2025/05/18)pytestにおけるMockの概要と実践

Posted at

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. ベストプラクティスとアンチパターン

  1. Arrange‑Act‑Assert の分離: Mock 設定(Arrange)を fixture 化し、テスト本体はビジネスロジック(Act)と結果検証(Assert)だけに。読みやすさ向上と重複削減。
  2. 黒箱テスト優先: 内部 private メソッド呼び出し数より、公共 API 出力 を検証。実装に依存しすぎるとリファクタで壊れやすい。
  3. オーバーモック禁止: DB も API も全モック化すると、「SQL が壊れても気付かない」テストになる。ユニット < 統合 < E2E のピラミッドを意識し、“どこまで嘘をつくか” をレイヤで線引き。
  4. autospec の活用: 引数違いを検知できるため、将来の API 変更でもテストが赤くなる=検知できる。
  5. 性能 vs 現実性: 非同期コードは AsyncMock を使い await 互換に。HTTPx/Pydantic/Eager evaluation など重い処理はレンジに応じて局所モック。

5. まとめ

  • MagicMock動的に作れるスタブ、複雑な呼び出しシナリオ再現に強い。
  • patch“import された名前” を差し替え、標準ライブラリやサードパーティ依存を無痛で置換。
  • monkeypatch は pytest 流儀で 値の上書きを簡潔に 行い、環境変数・CWD 操作に無敵。

Mock は刀。切れ味抜群だが振り回すと味方を斬る。外部依存を嘘にする範囲を最小化し、テストピラミッド全体で品質を担保しよう。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?