Python | テスト・設計・品質:monkeypatch

Python Python
スポンサーリンク

monkeypatch って何?mock とどう違うのか

pytest の monkeypatch は、
「テスト中だけ、モジュールやオブジェクトの属性を書き換えるための道具」です。

やりたいことは mock とほぼ同じで、

外部 API を呼ぶ関数を差し替えたい
現在時刻を固定したい
環境変数の値をテスト中だけ変えたい

といった「一時的なすり替え」です。

違いは、「pytest が用意しているフィクスチャとしての道具」だということ。
unittest.mock.patch が「標準ライブラリの関数」であるのに対して、
monkeypatch は「pytest がくれるオブジェクト」です。

感覚としては、

mock.patch
→ with 文やデコレータで「どこをどう差し替えるか」を書く

monkeypatch
→ テスト関数の中で「属性をこう書き換える」と命令する

というスタイルの違いです。


まずは一番シンプルな例:関数を差し替える

テストしたいコードを用意する

外部サービスからユーザー名を取ってくる関数を想像します。

# service.py

from external import get_user_name

def greet(user_id: int) -> str:
    name = get_user_name(user_id)
    return f"Hello, {name}!"
Python

external.get_user_name は、本当は外部 API を叩くとします。
テストでは、そこを本物で動かしたくないので、
「テスト中だけニセの get_user_name にすり替えたい」という状況です。

monkeypatch で関数を差し替える

pytest のテストで、monkeypatch フィクスチャを引数に受け取ります。

# test_service.py

from service import greet

def test_greet_with_fake_user(monkeypatch):
    def fake_get_user_name(user_id: int) -> str:
        return "Taro"

    monkeypatch.setattr("service.get_user_name", fake_get_user_name)

    msg = greet(123)
    assert msg == "Hello, Taro!"
Python

ここで起きていることを丁寧に分解します。

monkeypatch は pytest が自動で渡してくれる特別なオブジェクト
monkeypatch.setattr("service.get_user_name", fake_get_user_name) と書くと、
service モジュールの中の get_user_name を、テスト中だけ fake_get_user_name に置き換える」

という命令になります。

重要なのは、"service.get_user_name" と「import している側」を指定していることです。
これは mock.patch と同じルールで、「使っている側の名前を差し替える」が正解です。

テストが終わると、pytest が自動で元に戻してくれます。
withfinally を書かなくていいのが、monkeypatch の気持ちよさです。


monkeypatch と mock.patch の違いを感覚でつかむ

スタイルの違い

同じことを mock.patch で書くと、こうなります。

from unittest.mock import patch
from service import greet

def test_greet_with_fake_user():
    def fake_get_user_name(user_id: int) -> str:
        return "Taro"

    with patch("service.get_user_name", fake_get_user_name):
        msg = greet(123)

    assert msg == "Hello, Taro!"
Python

やっていることはほぼ同じですが、

mock.patch
→ with 文のブロックの中だけ差し替えが有効

monkeypatch
→ テスト関数の中で命令しておけば、そのテスト中はずっと有効(テスト終了時に自動で戻る)

という違いがあります。

pytest を前提にするなら、
「フィクスチャとして渡される monkeypatch を使う」方が自然なスタイルになりやすいです。

monkeypatch は「属性を書き換える道具」として割り切る

mock.patch は、MagicMock と組み合わせて
「呼び出し回数を検証する」「引数を検証する」などもよくやります。

monkeypatch は、もっとシンプルに、

この属性を、この値(関数・オブジェクト)に差し替える
この環境変数を、この値に変える

といった「書き換え」に特化した道具だと考えると分かりやすいです。

呼び出し回数などを検証したければ、
差し替える先に MagicMock を使えば OK です。


環境変数を monkeypatch でいじる(よくあるパターン)

テストしたいコード

例えば、環境変数から設定値を読む関数があるとします。

# config.py

import os

def get_api_key() -> str:
    return os.environ["API_KEY"]
Python

テストでは、「環境変数 API_KEY の値をテスト中だけ変えたい」ということがよくあります。

monkeypatch.setenv / monkeypatch.delenv を使う

# test_config.py

from config import get_api_key

def test_get_api_key(monkeypatch):
    monkeypatch.setenv("API_KEY", "TEST-KEY-123")

    assert get_api_key() == "TEST-KEY-123"
Python

monkeypatch.setenv("API_KEY", "TEST-KEY-123") と書くと、

テスト中だけ os.environ["API_KEY"] の値が "TEST-KEY-123" になる
テストが終わると、自動で元の値に戻る(なかった場合は削除される)

という挙動になります。

環境変数を直接いじると、
他のテストに影響が出たり、元に戻し忘れたりしがちですが、
monkeypatch を使えば「テストごとにクリーンな状態」を保ちやすくなります。

環境変数を消したいときは monkeypatch.delenv("API_KEY", raising=False) のように書けます。


現在時刻やランダムを固定する

テストしたいコード

例えば、「現在時刻を文字列にして返す」関数があるとします。

# clock.py

from datetime import datetime

def now_str() -> str:
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
Python

このままだと、テストするたびに結果が変わってしまいます。
「テスト中だけ、datetime.now() の結果を固定したい」という場面です。

monkeypatch で datetime.now を差し替える

# test_clock.py

from datetime import datetime
from clock import now_str

def test_now_str(monkeypatch):
    class DummyDateTime(datetime):
        @classmethod
        def now(cls, tz=None):
            return cls(2024, 1, 2, 3, 4, 5)

    monkeypatch.setattr("clock.datetime", DummyDateTime)

    assert now_str() == "2024-01-02 03:04:05"
Python

ここでのポイントは、

clock.py の中では from datetime import datetime としている
つまり、clock モジュールの中では datetime.now() と呼んでいる
だから monkeypatch.setattr("clock.datetime", DummyDateTime) と書いて、
clock モジュール内の datetime クラス自体を差し替えている

というところです。

これで、now_str() を呼んだときに、
常に「2024-01-02 03:04:05」が返るようになります。

現在時刻や乱数のような「毎回変わるもの」をテストするとき、
monkeypatch で「テスト中だけ固定する」のはよく使うテクニックです。


monkeypatch を使うときに意識してほしいポイント

「どの名前を差し替えるか」を正しく選ぶ

mock と同じく、monkeypatch でも一番ハマりやすいのはここです。

import している側のモジュール名.シンボル名を指定する
from xxx import yyy なら、そのモジュールの中の yyy を差し替える

というルールを守ること。

例えば、

service.pyfrom external import get_user_name と書いているなら、
monkeypatch.setattr("service.get_user_name", fake) が正解で、
monkeypatch.setattr("external.get_user_name", fake) では効きません。

「テスト対象のコードの中で、どんな名前で呼ばれているか」を意識する癖をつけると、
monkeypatch も mock も一気に扱いやすくなります。

「テストごとに元に戻る」ことを前提に設計する

monkeypatch は、テストが終わると自動で元に戻してくれます。
これはつまり、

テストごとにクリーンな状態で始まる
他のテストに影響を残さない

という前提でテストを書ける、ということです。

逆に言うと、「グローバルな状態を書き換える」ようなコードは、
monkeypatch なしだとテストが壊れやすくなります。

グローバル変数
モジュールレベルの設定
環境変数

などをいじるコードは、
「テスト中だけ monkeypatch で差し替える」前提で設計しておくと、
テストの安定性がかなり上がります。


まとめ(monkeypatch は「pytest 流のすり替えツール」)

pytest の monkeypatch を初心者目線で整理すると、こうなります。

pytest がくれる monkeypatch フィクスチャを使って、モジュールやオブジェクトの属性を「テスト中だけ」別のものに差し替えられる。
setattr で関数やクラスを差し替え、setenv / delenv で環境変数をいじれる。テスト終了時には自動で元に戻る。
mock.patch と同じく、「import している側の名前」を差し替えるのが重要なポイント。
外部 API、環境変数、現在時刻、乱数など「外部や変動するもの」をテストしやすくするための、pytest らしいシンプルな道具。

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