現代のアプリケーション開発において、外部のWeb API(例: 決済サービス、気象情報、SNS投稿)と連携する機能は不可欠です。しかし、このような外部サービスと通信するコードの自動テストを書こうとすると、多くの開発者が頭を悩ませます。「テストを実行するたびに、本当に外部APIを呼び出すべきなのだろうか?」と。
結論から言えば、その答えは**「いいえ」**です。ユニットテストで本物の外部APIを呼び出すことは、テストを不安定で、低速で、危険なものにするアンチパターンです。
今回は、この問題を解決するための必須テクニックである**「モック(Mock)」**を使い、テストを外部環境から完全に隔離する方法を解説します。
問題点:外部環境に依存したテストの数々の欠点
ある都市の現在の気温を、外部の気象情報APIから取得する関数を考えてみましょう。
テスト対象のコード (weather_service.py
):
import os
import requests
API_ENDPOINT = "https://api.weather.example.com/v1/current"
API_KEY = os.environ.get("WEATHER_API_KEY")
def get_current_temperature(city: str) -> float:
"""指定された都市の現在の気温をAPIから取得する。"""
params = {"city": city, "key": API_KEY}
response = requests.get(API_ENDPOINT, params=params)
response.raise_for_status() # 4xx or 5xx エラーの場合は例外を発生
return response.json()["temperature"]
この関数に対し、何も考えずにテストを書くと、次のようになります。
悪いテストの例 (tests/test_weather_service.py
):
from weather_service import get_current_temperature
def test_get_current_temperature_with_real_api():
# 実際にネットワーク通信が発生する!
temperature = get_current_temperature("Tokyo")
assert isinstance(temperature, float)
このテストは、多くの深刻な問題を抱えています。
- 不安定: 外部APIのサーバーがダウンしていたり、ネットワークの調子が悪かったりすると、あなたのコードが正しくてもテストは失敗します。
- 低速: ネットワーク通信の待ち時間(レイテンシ)により、テストの実行が非常に遅くなります。
- コスト: 多くのAPIは、呼び出し回数に応じて料金が発生します。
- 危険: もしこれがデータを書き込む
POST
リクエストなら、テストを実行するたびに本番環境にゴミデータが作成されてしまいます。 - 不完全: APIが
404 Not Found
や500 Server Error
を返した場合の、エラーハンドリングのロジックをテストすることが困難です。
解決策:モックで外部APIの「ふり」をさせる
これらの問題を解決するのが「モック」です。モックとは、本物のオブジェクト(ここではrequests
ライブラリ)の「ふり」をする偽物のオブジェクトのことです。テスト中は、この偽物に差し替えることで、外部との通信を完全に遮断し、テストをコントロール下に置きます。
Pythonの標準ライブラリであるunittest.mock
のpatch
機能を使ってみましょう。
モックを使った良いテストの例:
from unittest.mock import patch
import pytest
from requests.exceptions import HTTPError
from weather_service import get_current_temperature
# `weather_service`モジュール内の`requests`をモックに差し替える
@patch("weather_service.requests")
def test_get_current_temperature_success(mock_requests):
"""APIが正常な値を返した場合のテスト。"""
# Arrange (準備): モックが返す「偽のレスポンス」を設定
mock_requests.get.return_value.status_code = 200
mock_requests.get.return_value.json.return_value = {
"city": "Tokyo",
"temperature": 25.5,
}
# Act (実行): テスト対象の関数を呼び出す
temperature = get_current_temperature("Tokyo")
# Assert (検証):
# 1. 関数が、モックが返した値を正しく解釈できたか
assert temperature == 25.5
# 2. 意図した通りにrequests.getが呼び出されたか
mock_requests.get.assert_called_once_with(
"https://api.weather.example.com/v1/current",
params={"city": "Tokyo", "key": API_KEY}
)
このテストでは、実際にネットワーク通信は一切発生しません。get_current_temperature
がrequests.get
を呼び出すと、patch
によって用意されたモックオブジェクトがそれを捕らえ、我々が事前に定義した偽のレスポンスを返します。これにより、テストは高速かつ安定的に実行できます。
モックの真価:異常系のテスト
モックの最大の利点の一つは、意図的にエラー状況を作り出せることです。
@patch("weather_service.requests")
def test_get_current_temperature_handles_api_error(mock_requests):
"""APIがエラーを返した場合に、関数が正しく例外を投げるかのテスト。"""
# Arrange: モックに「404エラーのふり」をさせる
mock_requests.get.return_value.raise_for_status.side_effect = HTTPError
# Act & Assert: HTTPErrorが発生することを検証
with pytest.raises(HTTPError):
get_current_temperature("InvalidCity")
このように、モックを使えば、現実には再現が難しい様々な異常系パターンを網羅的にテストできます。
ユニットテストとインテグレーションテスト
注意点として、モックを使ったテストは、あくまで**「自分たちのコードが、APIからのレスポンスを正しく処理できるか」を検証するユニットテスト**です。
これとは別に、**「実際に外部APIと正しく通信できるか」を検証するインテグレーションテスト(結合テスト)**も必要です。ただし、こちらは実行コストが高いため、CI環境で一日一回だけ実行するなど、頻度を絞って行うのが一般的です。
まとめ
信頼性の高いテストスイートを構築するための鉄則は、ユニットテストを外部環境から完全に隔離することです。
- コードが外部のAPI、データベース、ファイルシステムなどとやり取りする場合、その部分をモックに差し替える。
- モックを使うことで、テストは高速、安定的、そして網羅的になる。
unittest.mock
はPythonの標準機能であり、responses
やpytest-mock
といったライブラリも強力な選択肢。
外部依存を適切にモックするスキルは、プロフェッショナルなテストコードを書くための必須のテクニックです。