【Python】API連携のテストはどう書く?「モック」で外部依存を断ち切る方法

現代のアプリケーション開発において、外部の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 Found500 Server Errorを返した場合の、エラーハンドリングのロジックをテストすることが困難です。

解決策:モックで外部APIの「ふり」をさせる

これらの問題を解決するのが「モック」です。モックとは、本物のオブジェクト(ここではrequestsライブラリ)の「ふり」をする偽物のオブジェクトのことです。テスト中は、この偽物に差し替えることで、外部との通信を完全に遮断し、テストをコントロール下に置きます。

Pythonの標準ライブラリであるunittest.mockpatch機能を使ってみましょう。

モックを使った良いテストの例:

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_temperaturerequests.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の標準機能であり、responsespytest-mockといったライブラリも強力な選択肢。

外部依存を適切にモックするスキルは、プロフェッショナルなテストコードを書くための必須のテクニックです。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次