外部API の呼び出しをテストするとき、毎回実際のサーバーに接続するのは遅いし、課金も心配ですよね。
さらに、API の仕様が変わったり、接続が不安定になったりすると、テストが不安定になり、本当にコードに問題があるのか判断しづらくなります。
そんなときに活躍するのが pytest のモック機能 です。外部API の呼び出しを模擬して、確実で高速なテストを実現できます。
本記事では、実務で使えるモックのパターンを5つ紹介し、各パターンのコード例と使い分けポイントを解説します。
pytest モック とは:基本概念から理解する
モック(Mock)は、実際の外部API の代わりに、テスト用の模擬オブジェクトを用意する手法です。
pytest で使えるモックライブラリは複数ありますが、最も一般的なのは Python 標準の unittest.mock モジュール と、それを拡張した pytest-mock プラグインです。
モックを使うメリットは以下のとおりです。
- テストが高速化される(ネットワーク遅延がない)
- 外部API の課金が発生しない
- API が変わってもテストコードは影響を受けない
- ネットワーク障害の影響を受けない
- 複雑なシナリオ(エラー応答など)を簡単に再現できる
実践パターン1:requests ライブラリを使った外部API 呼び出しのモック
最も基本的なパターンです。requests ライブラリで外部API を呼び出すコードをテストします。
テスト対象のコードは、以下のとおりです。
import requests
def get_user_data(user_id):
"""ユーザー情報を外部API から取得"""
url = f"https://api.example.com/users/{user_id}"
response = requests.get(url)
response.raise_for_status()
return response.json()
テストコードでは、requests.get をモックして、実際には API を呼び出さず、模擬データを返します。
import pytest
from unittest.mock import patch, MagicMock
def test_get_user_data(mocker):
"""requests.get をモックしてユーザー情報を取得"""
mock_response = MagicMock()
mock_response.json.return_value = {
"id": 1,
"name": "Taro Yamada",
"email": "taro@example.com"
}
mocker.patch("requests.get", return_value=mock_response)
result = get_user_data(1)
assert result["name"] == "Taro Yamada"
assert result["email"] == "taro@example.com"
ポイントは mocker.patch() でターゲットのモジュールを指定することです。
実践パターン2:複数の戻り値を返すシーケンシャルモック
API を複数回呼び出すテストでは、呼び出すたびに異なる結果を返す必要があります。
def get_multiple_users(user_ids):
"""複数のユーザー情報を順番に取得"""
users = []
for user_id in user_ids:
user = get_user_data(user_id)
users.append(user)
return users
このコードをテストするとき、3回の API 呼び出しで異なるデータを返す設定が必要です。
def test_get_multiple_users(mocker):
"""複数呼び出しで異なるデータを返す"""
mock_responses = [
{"id": 1, "name": "Taro"},
{"id": 2, "name": "Hanako"},
{"id": 3, "name": "Jiro"}
]
mock_response = MagicMock()
mock_response.json.side_effect = mock_responses
mocker.patch("requests.get", return_value=mock_response)
result = get_multiple_users([1, 2, 3])
assert len(result) == 3
assert result[0]["name"] == "Taro"
assert result[1]["name"] == "Hanako"
assert result[2]["name"] == "Jiro"
side_effect を使うと、呼び出すたびに異なる値を返せます。
実践パターン3:API エラーレスポンスのモック
実際の運用では、API が エラーを返すこともあります。そのときのエラーハンドリングをテストする必要があります。
def get_user_data_safe(user_id):
"""エラーハンドリング付きでユーザー情報を取得"""
try:
url = f"https://api.example.com/users/{user_id}"
response = requests.get(url, timeout=5)
response.raise_for_status()
return response.json()
except requests.exceptions.HTTPError as e:
print(f"HTTP エラー: {e.response.status_code}")
return None
except requests.exceptions.RequestException as e:
print(f"リクエストエラー: {e}")
return None
404 やタイムアウトの場合をテストします。
import requests
def test_get_user_data_not_found(mocker):
"""404 エラーの場合をテスト"""
mock_response = MagicMock()
mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError("404 Not Found")
mocker.patch("requests.get", return_value=mock_response)
result = get_user_data_safe(999)
assert result is None
def test_get_user_data_timeout(mocker):
"""タイムアウトの場合をテスト"""
mocker.patch(
"requests.get",
side_effect=requests.exceptions.Timeout("Connection timeout")
)
result = get_user_data_safe(1)
assert result is None
こうすることで、エラーケースも安全にテストできます。
実践パターン4:call_args_list で API 呼び出しの検証
モックは単に戻り値を返すだけでなく、API が正しく呼ばれたかを検証 することも重要です。
def test_verify_api_call(mocker):
"""正しいパラメータで API が呼ばれたか検証"""
mock_get = mocker.patch("requests.get")
mock_get.return_value.json.return_value = {"id": 1}
get_user_data(1)
# API が正しいURL で呼ばれたか確認
mock_get.assert_called_once_with("https://api.example.com/users/1")
# 呼び出し回数の検証
assert mock_get.call_count == 1
# 呼び出し時の引数の検証
call_args = mock_get.call_args
assert "api.example.com" in call_args[0][0]
これにより、コードが期待どおりに API を呼び出しているかを確認できます。
実践パターン5:fixture を使った再利用可能なモック設定
複数のテストで同じモック設定を使う場合、pytest の fixture を活用 すると DRY 原則を守れます。
import pytest
from unittest.mock import MagicMock
@pytest.fixture
def mock_api_response(mocker):
"""API レスポンスのモック fixture"""
mock_response = MagicMock()
mock_response.json.return_value = {
"id": 1,
"name": "Test User",
"email": "test@example.com"
}
mocker.patch("requests.get", return_value=mock_response)
return mock_response
@pytest.fixture
def mock_api_error(mocker):
"""API エラーのモック fixture"""
mocker.patch(
"requests.get",
side_effect=requests.exceptions.HTTPError("500 Server Error")
)
def test_with_success(mock_api_response):
"""成功ケースのテスト"""
result = get_user_data(1)
assert result["name"] == "Test User"
def test_with_error(mock_api_error):
"""エラーケースのテスト"""
result = get_user_data_safe(1)
assert result is None
fixture を使うと、テストコードがシンプルになり、保守もしやすくなります。
pytest モック と実装パターンの比較表
| パターン | 使用場面 | 難易度 | 再利用性 |
|---|---|---|---|
| 基本的なパッチ | 単純な API 呼び出し | 低 | 低 |
| side_effect | 複数回の異なる戻り値 | 中 | 中 |
| 例外エラー | エラーハンドリングのテスト | 中 | 低 |
| call_args_list 検証 | API 呼び出しの確認 | 中 | 中 |
| fixture | 複数テスト間の共通設定 | 中 | 高 |
実践での注意点と Tips
モック機能は便利ですが、いくつかの注意点があります。
モック範囲を意識することが大切です。モックの対象を間違えると、テストが通っても実際には動かない可能性があります。
たとえば、requests.get をモックするときは、対象のモジュール内での import 方法に注意が必要です。
# my_module.py 内で
from requests import get # この場合は my_module.get をモックする
# または
import requests # この場合は requests.get をモックする
テスト環境と本番環境を分離する のも重要です。製造業の中小企業がAIで品質管理を自動化 する場合も、AI システムと外部API の連携テストでは、モックを活用して安全にテストします。
モック側とコード側の整合性を保つ ことも忘れずに。API の仕様が変わったら、モックの設定も更新する必要があります。
実際のプロジェクトへの応用
これらのモック技法は、単なる単体テストにとどまりません。
中小企業のChatGPT導入完全ガイド では、外部の LLM API を呼び出すコードのテストでも同じ手法が活躍します。
ChatGPT API や他の AI サービスの呼び出しをモックすることで、開発中の課金を避けながら、堅牢なテストを書けます。
さらに、個人事業主がChatGPTで売上を40%増やした実例 のように、複数の外部API を組み合わせるとき、各API をモックして組み合わせテストを行えば、統合の問題を早期に発見できます。
CI/CD パイプラインに pytest を統合する場合も、モックの活用は必須です。パイプライン内では外部サービスへのアクセスが制限されることが多いため、モックがあれば自動テストを確実に実行できます。
おすすめ書籍・ガジェット
- pytest入門ガイド:pytest の基本から応用までを体系的に学べる、実務向けの教科書です。モック機能の詳しい解説も充実しています。
- Python単体テスト実践:unittest.mock の詳細と、テスト駆動開発(TDD)の考え方を学べる良書です。
- HHKB Professional:プログラミングの長時間作業に最適なキーボード。快適なコーディング環境は、テストコードの品質向上にもつながります。
まとめ:pytest モック を活用した効率的なテスト
pytest のモック機能を正しく使えば、外部API に依存しない、高速で信頼性の高いテストが実現できます。
5つのパターンを紹介しましたが、最初は基本的なパッチから始めて、プロジェクトに応じて複雑なパターンに進んでいくのがおすすめです。
特に、現代のソフトウェア開発では、複数の外部API を組み合わせることが当たり前になっています。中小企業のホームページをAI集客で自動化する実践チュートリアル のように、複合的なシステムを構築する場合、モックの活用が不可欠です。
本記事のコード例を参考に、実プロジェクトでモック機能を活用してください。テストの品質が向上すれば、本番環境でのバグは大幅に減り、開発スピードも上がります。
pytest のモック と unittest.mock の違いは何ですか?
pytest 自体にはモック機能がなく、pytest-mock プラグインを使って unittest.mock を拡張しています。pytest-mock は pytest の fixture 機能と統合されており、より簡潔で読みやすいテストコードが書けます。具体的には、mocker という fixture を直接使えるため、import や setup コードが減り、テストに集中できます。
モックを使うとなぜテストが高速化されるのですか?
外部API の呼び出しには通常、ネットワークのラウンドトリップ時間(数百ミリ秒〜数秒)が発生します。モックを使えば、この遅延が完全に排除され、ネットワークアクセスなしに瞬時に結果が返されます。数百個のテストケースがある場合、全体の実行時間が数分から数秒に短縮されることもあります。
モックをした場合、実際の API との連携は確認できないのではないですか?
その通りです。そのため、単体テスト(ユニットテスト)ではモックを使い、統合テスト(インテグレーションテスト)では実際の API またはステージング環境と連携テストを行うのが一般的です。異なるテスト段階で異なるテスト方法を使い分けることで、開発効率と品質の両立が実現できます。
side_effect と return_value の使い分けはどのようにしますか?
return_value は、モックが呼ばれるたびに同じ値を返します。一方、side_effect はリストやイテレータを指定すると、呼ばれるたびに異なる値を返すか、例外を発生させます。複数回の呼び出しで異なる結果が必要な場合や、エラーを模擬したい場合は side_effect を使用します。
CI/CD パイプラインでモックを使う際の注意点は?
CI/CD パイプライン内のテストではネットワークアクセスが制限されることが多いため、モックを積極的に使うべきです。ただし、定期的(夜間など)に実際の API との統合テストを別途実行し、本当に API と連携できるか確認することが重要です。モック単体では、API 仕様の変更に気付けないリスクがあります。
関連記事・さらに学習を深めるなら
外部API のテストについてさらに学びたい場合、ローカルLLM Ollamaの使い方 の記事では、ローカル環境で LLM を動かす方法を解説しており、外部サービスに依存しないテスト環境の構築方法も参考になります。
さらに、プロジェクト内で複数のエンジニアが協力して開発する場合、テストの統一性を保つことが重要です。本記事のパターンをチームのコーディング規約に取り込むことで、保守性の高いテストコードが実現できます。
pytest のモック機能は、Python での単体テストの鍵となるスキルです。ぜひ実務で活用してください。