こんにちは、かつコーチです。
外部APIを呼び出す処理のテストを書こうとして、こんな悩みに直面したことはありませんか。
「テストのたびに本物のAPIを叩くと、料金がかかるし遅い」
「ネットワークが切れているとテストが落ちる」
この記事では、こうした悩みを解決するモックの使い方を、unittest.mockを中心に解説します。
patchの基本的な使い方から、外部API呼び出しをモック化する実例まで、実践的な内容です。
読み終える頃には、外部依存を切り離した安定したテストが書けるようになります。
モックとは何か
モックの定義
モックとは、本物のオブジェクトの代わりに使う「偽物」のことです。
外部APIやデータベースなど、テストのたびに呼び出すと時間やコストがかかる処理を、モックに置き換えます。
モックは「呼ばれたら決まった値を返す」ように設定でき、本物を呼ばずにテストを完結させられます。
なぜモックが必要なのか
外部APIをそのまま呼び出すテストには、次のような問題があります。
- 通信環境によってテスト結果が変わる(不安定)
- API側の料金が発生する場合がある
- API側が障害中だと、自分のコードが正しくてもテストが失敗する
モックを使えば、これらの外部要因を排除し、自分のコードのロジックだけを検証できます。
patchの使い方
手順1:テスト対象の関数を用意する
外部APIから天気情報を取得する関数を例にします。
# weather.py
import requests
def get_temperature(city):
"""指定した都市の気温を取得する"""
response = requests.get(f"https://api.example.com/weather/{city}")
data = response.json()
return data["temperature"]
手順2:unittest.mock.patchでrequests.getを置き換える
unittest.mock.patchを使うと、requests.getを一時的に別の動きへ差し替えられます。
# test_weather.py
from unittest.mock import patch, MagicMock
from weather import get_temperature
@patch("weather.requests.get")
def test_get_temperature(mock_get):
# requests.get()の戻り値をモック化する
mock_response = MagicMock()
mock_response.json.return_value = {"temperature": 25}
mock_get.return_value = mock_response
result = get_temperature("tokyo")
assert result == 25
mock_get.assert_called_once_with("https://api.example.com/weather/tokyo")
pytest test_weather.py -v
test_weather.py::test_get_temperature PASSED [100%]
@patch("weather.requests.get")のポイントは、weatherモジュールの中で使われているrequests.getを狙って指定していることです。
mock_get.assert_called_once_with(...)で、正しいURLで1回だけ呼ばれたかまで検証できます。
手順3:例外が発生するケースもモック化する
APIがエラーを返すケースも、モックなら簡単に再現できます。
@patch("weather.requests.get")
def test_get_temperature_api_error(mock_get):
mock_get.side_effect = ConnectionError("接続に失敗しました")
import pytest
with pytest.raises(ConnectionError):
get_temperature("tokyo")
side_effectに例外を渡すと、その関数が呼ばれた瞬間に例外を発生させられます。
本物のAPIをわざと落とすことなく、異常系のテストが書けるのがモックの強みです。
つまずきやすい設定・注意点
patchの第一引数には、「モジュールが実際にimportしている場所」を指定する必要があります。
weather.pyがimport requestsとしているなら、パス指定は"weather.requests.get"です。
"requests.get"のように元のライブラリ名で指定すると、意図した箇所が差し替わりません。
よくあるつまずきポイント・エラー対処
patchの指定パスを間違えてモックが効かない
筆者が実際にハマったのが、patchの対象パスを元のライブラリ名で書いてしまったケースです。
❌ Before(モックが効かない書き方)
from unittest.mock import patch, MagicMock
from weather import get_temperature
@patch("requests.get") # weather.pyの名前空間を指定していない
def test_get_temperature(mock_get):
mock_response = MagicMock()
mock_response.json.return_value = {"temperature": 25}
mock_get.return_value = mock_response
result = get_temperature("tokyo")
assert result == 25
実行すると、本物のrequests.getが呼ばれてしまい、次のようなエラーが出ました。
requests.exceptions.ConnectionError: Failed to establish a new connection
モックを設定したはずなのに、実際のネットワーク接続が発生しています。
これは、weather.py側ではweatherモジュールの名前空間の中にあるrequests.getが呼ばれているのに、patch("requests.get")が別の場所を差し替えてしまっているためです。
✅ After(正しい書き方)
from unittest.mock import patch, MagicMock
from weather import get_temperature
@patch("weather.requests.get") # weather.py内での参照先を指定
def test_get_temperature(mock_get):
mock_response = MagicMock()
mock_response.json.return_value = {"temperature": 25}
mock_get.return_value = mock_response
result = get_temperature("tokyo")
assert result == 25
patchのパスは「ライブラリの本来の置き場所」ではなく、「使っているモジュール側から見た参照名」で指定するのが鉄則です。
このルールを覚えてからは、モックが効かないというトラブルはほとんどなくなりました。
まとめ
この記事のポイント
- モックは、外部APIなどを本物の代わりに使う「偽物」のオブジェクト
unittest.mock.patchで、対象の関数やメソッドを一時的に置き換えられるreturn_valueで戻り値、side_effectで例外の発生を再現できるpatchのパスは「使っているモジュール側から見た参照名」を指定する- 外部依存を切り離すことで、通信環境に左右されない安定したテストになる
次に読むべき記事
- pytestの基本:テストの書き方入門
- unittestとpytestの違い・使い分け
- テストカバレッジを計測する方法(pytest-cov)
タグ: Python, 上級者向け, テスト