Pythonの勉強が進んで、そろそろテストを書いてみようという段階に来ると、たいてい同じところでつまずきます。テストしたい関数が、外の世界とつながっているという壁です。
たとえば天気予報のAPIを呼んで気温を返す関数を考えてみてください。テストを実行するたびに本物のサーバーへ通信して、返ってくる気温も日によって変わります。
これでは、何度実行しても同じ結果になるとは限りません。テストとして成立しないわけです。
この困りごとをほどくための道具が、モックです。この記事では、追加インストールなしで使えるPython標準の仕組みだけで、モックを書けるようになるところまで進みます。
テストが外の世界につながると、何が困るのか¶
最初に、何がそんなに問題なのかをはっきりさせておきます。テストの中から外部サービスを呼ぶと、性質の違う困りごとが同時に起こります。
ひとつめは、遅いことです。通信は1回あたり数百ミリ秒かかることもあり、テストが100本あれば体感できるほどの待ち時間になります。
ふたつめは、結果が安定しないことです。相手のサーバーが落ちていたり、通信制限にかかったりすると、コードを何も変えていないのにテストが赤くなります。
みっつめが、いちばん厄介です。テストのたびに本当にメールが飛んだり、課金が発生したりする可能性があります。
私は10年ほどエンジニアとして開発に関わってきましたが、テストコードから本番の決済APIを叩いてしまった現場を一度だけ見たことがあります。金額はごく小さかったものの、あのときの空気はいまでも忘れられません。
だからこそ、テストの中では外の世界を切り離す必要があります。その切り離し方が、これから説明する話の中心です。
モックは、本物のふりをする身代わり¶
モックというのは、本物の代わりに置く偽物の部品のことです。舞台でいえば代役に近い存在だと思ってください。
公式ドキュメントでは、テスト対象のシステムの一部をモックオブジェクトで置き換え、それがどう使われたかを検証できるライブラリだと説明されています。ここには大事な点がふたつ含まれています。
置き換えることと、検証できることです。モックは、本物の代わりに動くだけでなく、何回どんな引数で呼ばれたかを記録してくれます。
たとえば通信部分をモックに差し替えれば、決まった気温を必ず返すようにできます。テストは通信を待たず、毎回同じ結果になるというわけです。
そもそもテストの実行方法自体があやふやなら、先にそちらを固めておくと理解がぐっと早くなります。【関連記事】pytest入門|Pythonでテストを書く基本を初心者向けに解説
unittest.mockは最初から入っている¶
うれしいことに、モックのためのライブラリは標準で付いてきます。Python 3.3から標準ライブラリに加わったunittest.mockがそれです。
pipでのインストールは要りません。importするだけで使えます。
まずは、いちばん小さな例を動かしてみましょう。
from unittest.mock import Mock
# 身代わりのオブジェクトを作る
weather = Mock()
# 呼ばれたときに返す値を決める
weather.get_temperature.return_value = 25.5
print(weather.get_temperature("tokyo")) # 25.5
print(weather.get_temperature.call_count) # 1
get_temperatureというメソッドは、どこにも定義していません。それでも動くのがモックの特徴です。
Mockは、属性にアクセスされた時点で新しいモックを自動的に作ります。だから何を呼んでもエラーにならず、呼ばれた記録だけが残っていきます。
MockとMagicMockはどう違うのか¶
ドキュメントを読むと、MockとMagicMockという似た名前が出てきます。違いは、特殊メソッドがあらかじめ用意されているかどうかです。
MagicMockはMockのサブクラスで、__len__や__iter__といった特殊メソッドが最初から使える状態になっています。そのため、長さを測ったりforで回したりするコードのテストで扱いやすくなります。
迷ったらMagicMockを選んでおけば、たいていの場面で困りません。後で説明するpatchも、標準ではMagicMockを使います。
特殊メソッドというのは、Pythonが裏側で自動的に呼ぶ決まった名前のメソッドのことです。len関数を使うと__len__が呼ばれる、といった具合の仕組みだと思ってください。
return_valueとside_effectで振る舞いを決める¶
モックに何をさせるかは、ふたつの属性で決まります。決まった値を返すのか、呼ばれたときに何かを起こすのか、という違いです。
よく使う設定を表にまとめました。最初はこの5つだけ覚えれば足ります。
| 書き方 | 何をするか | 使いどころ |
|---|---|---|
mock.return_value = 25.5 |
呼ばれたら必ず25.5を返す | 成功したときの応答を固定する |
mock.side_effect = TimeoutError |
呼ばれたら例外を投げる | 通信失敗の処理を試す |
mock.side_effect = [1, 2, 3] |
呼ばれるたびに順番に返す | リトライ処理を試す |
mock.assert_called_once_with("tokyo") |
1回だけその引数で呼ばれたか検査する | 呼び出し方が正しいか確かめる |
mock.call_args |
最後に呼ばれたときの引数を取り出す | 引数を細かく調べたいとき |
side_effectには関数も渡せます。引数によって返す値を変えたいときは、そこで分岐させると素直に書けます。
patchで本物を一時的に差し替える¶
ここまではモックを手で作っていました。実務で必要になるのは、すでにコードの中で使われている本物を、テストの間だけ入れ替える操作です。
その役目を担うのがpatchです。デコレータとしても、with文のコンテキストマネージャーとしても使えます。
まず、テスト対象のコードを用意します。requestsで気温を取ってくる、よくある形です。
# weather.py
import requests
def get_temperature(city):
response = requests.get(f"https://api.example.com/weather/{city}")
response.raise_for_status()
return response.json()["temperature"]
このままテストすると本当に通信してしまいます。そこで、requests.getの部分だけを身代わりに差し替えます。
# test_weather.py
from unittest.mock import patch
import weather
def test_get_temperature():
with patch("weather.requests.get") as mock_get:
mock_get.return_value.json.return_value = {"temperature": 25.5}
mock_get.return_value.raise_for_status.return_value = None
assert weather.get_temperature("tokyo") == 25.5
mock_get.assert_called_once_with(
"https://api.example.com/weather/tokyo"
)
with文を抜けた瞬間に、requests.getは本物へ戻ります。テストが終わったあとに影響が残らないところが、patchの安心できる点です。
requestsそのものの動きがまだ曖昧なら、先に基本をさらっておくとコードの意味がはっきりします。【関連記事】Pythonのrequestsとは?WebからデータをとってくるHTTP通信の基本を初心者向けに解説
どこをpatchするかで結果が変わる¶
patchでいちばん引っかかるのが、差し替える場所の指定です。上の例でrequests.getではなくweather.requests.getと書いたのには理由があります。
公式ドキュメントは、patchは名前が指しているものを一時的に別のものへ変える仕組みだと説明しています。そのうえで、テスト対象が実際に使っている名前を差し替える必要があると書かれています。
つまり、定義された場所ではなく、使われている場所を狙うということです。モジュールbがモジュールaからクラスを取り込んで使っているなら、差し替えるのはb.SomeClassのほうになります。
モックが効かないと感じたときは、まずこのpatch先の指定を疑ってください。私の経験でも、モック関連のつまずきの半分以上はここが原因でした。
呼ばれ方まで確かめると、テストが強くなる¶
モックは値を返すだけの道具ではありません。どう呼ばれたかを記録している点が、実はいちばんの価値です。
たとえばキャッシュを実装したなら、2回目の呼び出しでAPIを叩かないことを確かめたいはずです。それは呼び出し回数を見れば検証できます。
例外が起きたときの動きも、side_effectを使えば手軽に再現できます。
import pytest
from unittest.mock import patch
import weather
def test_timeout_is_raised():
with patch("weather.requests.get", side_effect=TimeoutError):
with pytest.raises(TimeoutError):
weather.get_temperature("tokyo")
本物のサーバーをわざと落とすことはできませんが、モックなら1行で落ちた状態を作れます。異常系のテストが書きやすくなるのは、この性質のおかげです。
テストの前準備をきれいに切り出したくなったら、fixtureと組み合わせると見通しがよくなります。【関連記事】pytestのfixture入門|テスト前の準備処理をわかりやすく解説
pytestを使うなら、monkeypatchという道もある¶
pytestには、標準で使える別の差し替え手段が用意されています。monkeypatchという名前のfixtureです。
公式ドキュメントでは、属性や辞書の要素、環境変数を安全に設定または削除でき、sys.pathの変更もできると説明されています。そして変更はテストが終わったあとにすべて自動で取り消されるので、後片付けを忘れる心配がありません。
環境変数を差し替える場面では、こちらのほうが短く書けます。
def test_api_key_is_read(monkeypatch):
monkeypatch.setenv("API_KEY", "dummy-key-for-test")
import os
assert os.environ["API_KEY"] == "dummy-key-for-test"
ふたつの道具は競合するものではなく、得意分野が違うだけです。違いを表にしておきます。
| 観点 | unittest.mock | pytestのmonkeypatch |
|---|---|---|
| 使える場所 | 標準ライブラリなのでどこでも | pytestのテストの中だけ |
| 主な用途 | 関数やクラスを身代わりに置き換える | 属性・辞書・環境変数を書き換える |
| 呼ばれ方の記録 | 記録され、検証用のメソッドがある | 記録されない |
| 後片付け | with文やデコレータの範囲を抜けたら戻る | テストが終わったら自動で戻る |
呼ばれ方まで検証したいならunittest.mock、設定値を差し替えたいだけならmonkeypatch。この使い分けで、まず困ることはありません。
環境変数まわりの扱いに不安があるなら、基本を先に押さえておくと安全です。【関連記事】環境変数とは?PythonでAPIキーを安全に扱う.envと os.environ の基本を初心者向けに解説
モックを使いすぎると、何が起きるのか¶
便利な道具なので、つい何でもモックにしたくなります。ただ、そこには落とし穴があります。
モックは本物ではありません。だから、本物の仕様が変わってもモックは何も教えてくれません。
私が痛い目を見たのは、外部サービスのレスポンスの項目名が変わったときでした。テストは全部緑のまま通り続け、本番で初めて壊れていることに気づいたのです。
この事故を減らす仕組みが、autospecと呼ばれる機能です。モックが持てる機能を本物の設計に合わせて制限してくれるので、存在しないメソッドを呼んだ時点でテストが失敗します。
書き方は難しくありません。patch("weather.requests.get", autospec=True)のように引数を足すだけです。
もうひとつの目安として、自分が書いたコードはできるだけモックにせず、外部サービスや時刻のように制御できないものだけを差し替えるという線引きが役に立ちます。この基準にすると、テストが本物の動きから離れすぎません。
現在時刻に依存する処理をテストしたい場合も、考え方は同じです。日付まわりの扱いを整理しておくと、どこを差し替えるべきかが見えてきます。【関連記事】Pythonのdatetimeとは?日付と時刻の扱い方を初心者向けに解説
まとめ¶
モックは、テストの中で外の世界を切り離すための身代わりです。標準ライブラリのunittest.mockを使えば、追加インストールなしで今日から書けます。
覚えることは多くありません。return_valueで返す値を決め、side_effectで例外や連続した値を作り、patchで本物を一時的に差し替える。この3つが土台になります。
そしてpatchでは、定義された場所ではなく使われている場所を指定する。ここだけ意識しておけば、最初の壁はほとんど越えられます。
まずは手元のテストの中で、通信している行を1か所だけモックに置き換えてみてください。実行が一瞬で終わる感覚を味わうと、テストを書くのが少し楽しくなるはずです。
参考情報¶
- unittest.mock — mock object library(Python公式ドキュメント)
- unittest.mock — getting started(Python公式ドキュメントの入門ページ)
- How to monkeypatch/mock modules and environments(pytest公式ドキュメント)
ここまでお読みいただきありがとうございました。