テストを書こうとするたびに「外部APIの呼び出しどうすればいいんだ…」と手が止まりがちだったのですが、unittest.mock を使いこなせるようになってからかなりスムーズになりました。この記事ではそのあたりの基本を整理します。
このシリーズ「Python pytest 入門」では、pytest の基本的な使い方から始めて、モック・フィクスチャ・カバレッジ計測まで全4回で扱う予定です。
- 第1回(今回): unittest.mock でモックを使ったテストの基本
- 第2回: pytest fixture の使い方と scope の違い
- 第3回: pytest-mock を使ったより実践的なモックパターン
- 第4回: カバレッジ計測と CI への組み込み
第1回は unittest.mock の基礎から。pytest と組み合わせた使い方を中心に書いていきます。
この記事でわかること
unittest.mockがなぜ必要なのか、モックの基本的な考え方MagicMockの基本的な使い方@patchデコレータで外部依存を差し替える方法side_effectを使った例外やシーケンス制御- モックの呼び出しを検証するアサーションメソッド
そもそもモックって何のためにあるのか
テストを書くとき、困るのが「外の世界に依存している処理」です。たとえば:
- 外部 API を叩く関数
- データベースに書き込む処理
- ファイルシステムを操作するコード
- 現在時刻を返す
datetime.now()
こういった処理をそのままテストすると、ネットワーク状況やデータの状態によって結果が変わってしまいます。テストは「同じ条件なら常に同じ結果になる」べきなので、これだと困ります。
そこでモック(Mock)の出番です。モックは「本物の代わりに置く偽物」で、「このAPIを呼んだらこの値を返してほしい」という振る舞いを事前に設定できます。本物の処理は実行されないので、テストが高速になるし、外部依存もなくなる、というわけです。
余談ですが、モックを使いこなすには「テスト対象の境界をどこに引くか」をちゃんと意識する必要があって、それがまたけっこう難しいんですよね。この記事ではそこまで深入りせず、まず動かせることを優先します。
環境準備
pytest は別途インストールが必要ですが、unittest.mock は Python 3.3 以降の標準ライブラリに含まれているので追加インストール不要です。
pip install pytest
動作確認した環境は Python 3.12 / pytest 8.x としています。
MagicMock の基本的な使い方
unittest.mock の中でよく使うのが MagicMock です。まずは単体で触ってみます。
from unittest.mock import MagicMock
mock_func = MagicMock()
# 呼び出すと MagicMock が返ってくる
result = mock_func(42)
# 呼ばれたかどうかを検証
mock_func.assert_called_once_with(42)
# 返り値を指定したいとき
mock_func.return_value = "hello"
print(mock_func()) # "hello"
MagicMock はとにかく何でも受け付けてくれる便利な偽物です。属性アクセスも呼び出しも全部受け流してくれます。
Mock と MagicMock の違いは、後者がマジックメソッド(__len__ や __iter__ など)もあらかじめ定義している点です。基本的には MagicMock を使っておけば間違いないと思います。
@patch デコレータでテスト対象の依存を差し替える
実際のテストでよく使うのが @patch デコレータです。テスト実行中だけ特定のオブジェクトをモックに差し替えてくれます。
たとえば、こういうコードがあるとします:
# notifier.py
import requests
def notify_slack(message: str) -> bool:
response = requests.post(
"https://hooks.slack.com/services/xxx",
json={"text": message}
)
return response.status_code == 200
この関数をテストしたいのに、テストのたびに Slack に通知が飛んでしまっては困ります。@patch を使って requests.post をモックに差し替えます:
# test_notifier.py
from unittest.mock import MagicMock, patch
from notifier import notify_slack
@patch("notifier.requests.post") # ここ注意: インポート先のパスを指定する
def test_notify_slack_success(mock_post):
mock_post.return_value.status_code = 200
result = notify_slack("テスト通知")
assert result is True
mock_post.assert_called_once()
ポイントは @patch の引数です。requests.post ではなく notifier.requests.post と書く必要があります。「モックしたいオブジェクトが実際に使われている場所のパス」を指定するのがルールで、ここを間違えるとモックが効きません。最初ここでハマりました。
差し替え先のパスを間違えやすいケース
特に from xxx import yyy でインポートしている場合は注意が必要です。
# notifier_v2.py
from requests import post # ← こっちでインポートしている場合
def notify_slack(message: str) -> bool:
response = post("https://hooks.slack.com/services/xxx", json={"text": message})
return response.status_code == 200
この場合、パッチするのは requests.post ではなく notifier_v2.post になります:
@patch("notifier_v2.post") # ← from import した場合はこちら
def test_notify_slack_v2(mock_post):
mock_post.return_value.status_code = 200
assert notify_slack("テスト") is True
「なんでモックが効かないんだろう」ってなるやつの大半がこれだと思います。
with 構文を使った patch
デコレータの代わりに with 文で書くこともできます。テスト関数の中でモックの有効範囲を限定したいときに便利です。
from unittest.mock import patch
from notifier import notify_slack
def test_notify_slack_with_context():
with patch("notifier.requests.post") as mock_post:
mock_post.return_value.status_code = 200
result = notify_slack("テスト通知")
assert result is True
# with ブロックを出た後はモックが解除されている
個人的には、テスト関数が長くなってきたら with 構文のほうが「どこでモックが有効か」が視覚的にわかりやすくて好みです。
side_effect で例外やシーケンスを返す
return_value は「常に同じ値を返す」場合に使いますが、例外を発生させたいときや呼び出しごとに異なる値を返したいときは side_effect を使います。
from unittest.mock import MagicMock, patch
import pytest
from notifier import notify_slack
# 例外を発生させるテスト
@patch("notifier.requests.post")
def test_notify_slack_raises(mock_post):
mock_post.side_effect = ConnectionError("接続失敗")
with pytest.raises(ConnectionError):
notify_slack("テスト通知")
# 呼び出しごとに異なる値を返す
def test_side_effect_sequence():
mock = MagicMock()
mock.side_effect = [1, 2, 3]
assert mock() == 1
assert mock() == 2
assert mock() == 3
side_effect にリストを渡すと、呼び出すたびに順番に値が返ってきます。リストを使い切った後に呼び出すと StopIteration が上がるので、必要な分だけ用意しておくのがコツです。
呼び出しを検証する assert メソッド
モックが「正しく呼ばれたか」を検証するアサーション系のメソッドも整理しておきます。
from unittest.mock import MagicMock
mock = MagicMock()
mock("arg1", key="value")
# 一度だけ呼ばれたか
mock.assert_called_once()
# 指定の引数で一度だけ呼ばれたか
mock.assert_called_once_with("arg1", key="value")
# 直近の呼び出しが、指定の引数だったか
mock.assert_called_with("arg1", key="value")
# 一度も呼ばれていないか
mock.assert_not_called()
# 呼ばれた回数を直接確認
assert mock.call_count == 1
これだけ把握しておけばたいていのケースには対応できます。あと mock.call_args_list で全呼び出しの引数履歴が取れるので、複数回呼ばれた場合の詳細検証にも使えます。
※この記事にはプロモーションが含まれます
ちなみに、お名前.com レンタルサーバー(WordPressに特化した高速レンタルサーバー。月額990円〜、独自ドメイン実質0円)も気になっています。お名前.com レンタルサーバー![]()
まとめ
正直、最初は「なんでパスの書き方がこんなにわかりにくいんだ」と思っていましたが、「使われている場所を指定する」という原則を理解してからは迷わなくなりました。モックの仕組み自体はシンプルで、MagicMock + @patch + side_effect の組み合わせが使いこなせれば、実務レベルのほとんどのケースはカバーできると思います。
