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

# pytestにおけるMockの概要と実践


## はじめに

テストコードの最終的な価値は **速度・再現性・保守性** に帰着する。しかし実サービスでは外部 API、メッセージキュー、DB、システム時刻など“副作用を伴う依存先”が必ず存在し、これらを直接叩くとテストは遅く、不安定で、CI でも失敗しやすい。そこで登場するのが **Mock** だ。本稿では Python 標準 `unittest.mock` と pytest 固有の `monkeypatch` を軸として深掘りする。

> 実装詳細が見透かせるテストは魅力的だが、安易に使うと結合度が高まりリファクタ耐性が下がる。以下の内容は「**Mock の威力と落とし穴**」を両輪で捉えるためのガイドである。

---

## 1. `unittest.mock.MagicMock` ― 動的スタブ生成器

### 1‑1. コア概念

`MagicMock` は任意属性へのアクセス・呼び出しをフックし、**呼び出し履歴 (call args list) を自動記録** する動的オブジェクトだ。生成直後は“何も設定されていないが何でも返せる”シュレディンガーモックであり、**属性アクセスはすべて新たな `MagicMock` を生成して返却**する。

```python
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. テスト設計例

```python
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` 名前** である。

```python
# 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. 実践例: 非決定抽選関数を固定

```python
# 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 注入

```python
# 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 は刀。切れ味抜群だが振り回すと味方を斬る。**外部依存を嘘にする範囲を最小化し、テストピラミッド全体で品質を担保しよう。**