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

Python Python
スポンサーリンク

mock って何?まずはイメージから

mock(モック)は、テストのときだけ「本物の代わりに振る舞うニセモノのオブジェクト」です。
本物をそのまま使うと困る場面を想像してみてください。

外部 API を叩く(お金がかかる・遅い・回数制限がある)
メールを送る(本当に送られたら困る)
DB に書き込む(テストのたびにデータが増える)
現在時刻や乱数に依存する(結果が毎回変わる)

こういう「テストのたびに本物を動かしたくない」「制御できない」ものを、
テスト中だけ「ニセモノ」にすり替えてしまうのが mock です。

mock を使うと、

呼ばれたかどうか
何回呼ばれたか
どんな引数で呼ばれたか
どんな値を返すことにするか

を、テスト側から自由にコントロールできます。


まずは「外部 API を呼ぶ関数」を例にする

テストしたい関数(本物のままだと困るやつ)

例えば、requests を使って外部 API からユーザー情報を取ってくる関数があるとします。

# api_client.py

import requests

def fetch_user_name(user_id: int) -> str:
    url = f"https://example.com/api/users/{user_id}"
    resp = requests.get(url, timeout=5)
    resp.raise_for_status()
    data = resp.json()
    return data["name"]
Python

この関数をテストしたいとき、
本当に https://example.com にリクエストを飛ばしたくはないですよね。

ネットワーク環境に依存する
API 側の状態に依存する
テストを何度も回すと迷惑になる

など、テストが「遅い・不安定・迷惑」になりがちです。

ここで mock の出番です。


unittest.mock.patch の基本(関数をニセモノにすり替える)

requests.get を mock に置き換える

Python 標準ライブラリの unittest.mock を使います。
pytest でもそのまま使えます。

# test_api_client.py

from unittest.mock import patch, MagicMock
from api_client import fetch_user_name

def test_fetch_user_name():
    fake_response = MagicMock()
    fake_response.json.return_value = {"name": "Taro"}
    fake_response.raise_for_status.return_value = None

    with patch("api_client.requests.get", return_value=fake_response) as mock_get:
        name = fetch_user_name(123)

    assert name == "Taro"
    mock_get.assert_called_once_with("https://example.com/api/users/123", timeout=5)
Python

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

patch("api_client.requests.get", ...)
api_client モジュールの中で使われている requests.get を、テスト中だけ差し替える宣言です。
ポイントは「import している側の名前」を指定することです(ここは重要なので後で深掘りします)。

return_value=fake_response
fetch_user_name の中で requests.get(...) が呼ばれたとき、
本物の HTTP リクエストは飛ばずに、代わりに fake_response が返されます。

fake_response.json.return_value = {"name": "Taro"}
fake_response.json() が呼ばれたら、この辞書を返すように設定しています。

fake_response.raise_for_status.return_value = None
raise_for_status() が呼ばれても何も起きない(例外を投げない)ようにしています。

mock_get.assert_called_once_with(...)
「どんな URL と引数で呼ばれたか」を検証しています。
これにより、「正しい URL を組み立てているか」もテストできます。

つまり、テストでは、

HTTP 通信は一切していない
でも「通信したかのように」振る舞わせている
その上で「返り値」と「呼び出し方」を検証している

という状態になっています。


「どこを patch するか」が超重要なポイント

import している側を patch する、というルール

さっきの例で patch("api_client.requests.get", ...) と書いたのは、
かなり重要なポイントです。

api_client.py の中では、こう書いていました。

import requests

def fetch_user_name(...):
    resp = requests.get(...)
Python

つまり、fetch_user_name の中から見える名前は requests.get です。
この「見えている名前」を差し替える必要があります。

もしここで、

with patch("requests.get", ...)  # こう書くと…
Python

と書いてしまうと、
api_client モジュールの中の requests.get は差し替わりません。
api_client はすでに import requests 済みだからです。

mock で一番ハマりやすいのがここです。

「import している側のモジュール名.シンボル名を patch する」

というルールを、早めに体に染み込ませておくと、後々かなり楽になります。


MagicMock の基本的な振る舞いを押さえる

何をしてもとりあえず動く「何でも屋」

MagicMock は、「何をしてもとりあえずそれっぽく動く」オブジェクトです。

from unittest.mock import MagicMock

m = MagicMock()
m.foo()          # エラーにならない
m.bar(1, 2, 3)   # これもエラーにならない
print(m.baz)     # 属性アクセスもできる
Python

デフォルトでは、
どんなメソッドを呼んでも、どんな属性を触っても、
新しい MagicMock が返ってきます。

だからこそ、テストで使うときは、

return_value
side_effect
属性名.return_value

などを使って、「どう振る舞ってほしいか」を明示的に設定します。

呼び出し履歴を検証できる

MagicMock は、「どう呼ばれたか」の情報も覚えています。

m = MagicMock()
m.foo(1, 2, x=3)

m.foo.assert_called_once_with(1, 2, x=3)
Python

これにより、

この関数が呼ばれたか?
何回呼ばれたか?
どんな引数で呼ばれたか?

をテストで検証できます。

mock の本質は、「外部とのやりとり」をテストすることです。
返り値だけでなく、「呼び出し方」もテスト対象になる、という感覚を持っておくと強いです。


副作用のある処理を mock で封じる

メール送信を例にする

例えば、ユーザー登録時にメールを送る関数があるとします。

# notifier.py

def send_welcome_email(email: str) -> None:
    print(f"Send email to {email}")  # 本当は外部サービスを叩く想定

# service.py

from notifier import send_welcome_email

def register_user(email: str) -> None:
    # ここで DB にユーザーを作る想定
    send_welcome_email(email)
Python

register_user をテストするとき、
本当にメールを送る必要はありません。
「メール送信関数が呼ばれたかどうか」だけ分かれば十分です。

# test_service.py

from unittest.mock import patch
from service import register_user

def test_register_user_sends_email():
    with patch("service.send_welcome_email") as mock_send:
        register_user("taro@example.com")

    mock_send.assert_called_once_with("taro@example.com")
Python

ここでは、

service モジュールの中で使われている send_welcome_email を差し替える
実際には何も送らない(中身は空の mock)
「呼ばれたかどうか」「どんな引数で呼ばれたか」だけを検証する

という形になっています。

これが mock の典型的な使い方です。


mock を使うときに意識してほしい設計の感覚

「外部との境界」を意識する

mock が必要になるのは、だいたい「外部との境界」です。

ネットワーク(HTTP、gRPC など)
ファイルシステム
DB
メール・キュー・メッセージング
現在時刻・乱数・環境変数

こういった「外の世界」に触る部分を、
コードの中でちゃんと分離しておくと、
そこだけを mock で差し替えやすくなります。

逆に、ビジネスロジックの中に直接 requests.getopen が散らばっていると、
テストで差し替えるのが大変になります。

「外部とのやりとりを行う関数(やクラス)」と
「ビジネスロジックの本体」を分ける設計は、
テストしやすさの面でも非常に重要です。

「何でもかんでも mock」は逆効果

mock は便利ですが、乱用するとテストが壊れやすくなります。

内部実装の細かい呼び出し方に依存しすぎる
実装を少し変えただけでテストが大量に壊れる
「何を保証したいテストなのか」が分からなくなる

という状態になりがちです。

mock を使うときは、

本当に外部とのやりとりを隠したいのか?
返り値だけでなく、「呼び出しが行われたこと」自体が仕様なのか?

を一度立ち止まって考えるといいです。

「純粋な計算ロジック」は、mock なしでテストできるようにしておくのが理想です。


まとめ(mock は「外の世界のニセモノ」を作る道具)

Python/pytest における mock を、初心者目線で整理するとこうなります。

本物を動かしたくない処理(外部 API、メール、DB、時刻など)を、テスト中だけニセモノにすり替える仕組みが mock。
unittest.mock.patch で「import している側の名前」を差し替え、MagicMock で返り値や呼び出し履歴をコントロールする。
返り値だけでなく、「何回・どんな引数で呼ばれたか」を検証できるので、「外部とのやりとり」を安全にテストできる。
mock を使う前提として、「外部との境界」と「ビジネスロジック」を分けた設計にしておくと、テストがシンプルになる。

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