Python | テスト・設計・品質:API のモック

Python Python
スポンサーリンク
  1. APIのモックって何?一言でいうと「本物の代わりに動く“テスト用の偽物サーバー”」
  2. まずは「外部APIをそのまま叩いているコード」の問題点を知る
    1. 悪い例:テストで本物のHTTPを叩いてしまう
  3. APIモックの基本アイデア:「requests.get をすり替える」
    1. unittest.mock.patch を使った最小のモック例
  4. 重要ポイント1:「外部APIを直接呼ぶコード」と「アプリ本体」を分ける
    1. 直接 requests を呼ぶ設計だと、モックがつらくなる
    2. 良い方向性:外部APIクライアントを「クラス」として切り出す
  5. 重要ポイント2:成功だけでなく「失敗パターン」をモックで再現する
    1. 本番でしか起きないエラーを、テストで再現できるようにする
  6. 重要ポイント3:「HTTPレベルのモック」と「クライアントレベルのモック」を使い分ける
    1. HTTPレベルのモック(requests-mock など)
    2. クライアントレベルのモック(インターフェースを差し替える)
  7. APIモックと設計・品質のつながり
    1. モックしやすいコードは「依存が外から注入されている」
    2. APIモックがあると「自動テストの範囲」を広げられる
  8. 初心者がAPIモックを身につけるためのステップ
    1. ステップ1:requestsを直接モックするテストを一つ書いてみる
    2. ステップ2:外部APIクライアントクラスを作り、Fakeクライアントでテストする
    3. ステップ3:エラー系(500・タイムアウト)のテストをモックで書いてみる
  9. まとめ(APIのモックは「外部サービスに頼らず、成功も失敗も自由に再現するための偽物づくり」)

APIのモックって何?一言でいうと「本物の代わりに動く“テスト用の偽物サーバー”」

API のモックは、
「本物の外部APIを呼ばずに、その代わりをする“偽物”を用意してテストすること」です。

本物のAPIは、遅い・お金がかかる・回数制限がある・壊れているかもしれない、などの理由で、テストで毎回叩くわけにはいきません。
そこで「こう返ってくるはず」という前提で、テスト中だけ“代役”を立てる——それがモックです。

テスト・設計・品質の観点では、

外部サービスに依存せずにテストできる
成功パターンだけでなく、エラーやタイムアウトも自由に再現できる
テストが速く・安定して・再現性高くなる

という意味で、かなり重要な技術です。


まずは「外部APIをそのまま叩いているコード」の問題点を知る

悪い例:テストで本物のHTTPを叩いてしまう

例えば、こんなコードがあるとします。

import requests

def fetch_user_name(user_id: int) -> str:
    url = f"https://example.com/api/users/{user_id}"
    resp = requests.get(url, timeout=5)
    resp.raise_for_status()
    data = resp.json()
    return data["name"]
Python

これをそのままテストすると、こうなります。

def test_fetch_user_name():
    name = fetch_user_name(1)
    assert name == "Taro"
Python

一見テストっぽいですが、問題だらけです。

インターネットにつながっていないと落ちる
example.com の都合でレスポンスが変わると落ちる
APIが遅いとテストも遅くなる
回数制限や料金があるAPIだと、テストを回すだけでコストがかかる

つまり、「テストが外部世界に依存している」状態です。
これでは、安定した自動テストの土台になりません。


APIモックの基本アイデア:「requests.get をすり替える」

unittest.mock.patch を使った最小のモック例

Python には unittest.mock という標準ライブラリがあり、
関数やメソッドを「テスト中だけ差し替える」ことができます。

先ほどの fetch_user_name をモック付きでテストしてみます。

import json
from unittest.mock import patch, Mock

from myapp.external import fetch_user_name

def test_fetch_user_name_uses_api_response():
    fake_response = Mock()
    fake_response.json.return_value = {"name": "Taro"}
    fake_response.raise_for_status.return_value = None

    with patch("myapp.external.requests.get", return_value=fake_response) as mock_get:
        name = fetch_user_name(1)

    assert name == "Taro"
    mock_get.assert_called_once_with("https://example.com/api/users/1", timeout=5)
Python

ここでやっていることを分解します。

patch("myapp.external.requests.get", ...)
fetch_user_name が内部で呼ぶ requests.get を、テスト中だけ差し替える。

fake_response
本物の requests.Response の代わりをするモックオブジェクト。
json()raise_for_status() の戻り値を自分で決めている。

これにより、

HTTPは一切飛ばない
レスポンスの中身を自由にコントロールできる
「どんなURLで呼ばれたか」も検証できる

という状態になります。

これが「APIのモック」の一番素朴な形です。


重要ポイント1:「外部APIを直接呼ぶコード」と「アプリ本体」を分ける

直接 requests を呼ぶ設計だと、モックがつらくなる

さっきの fetch_user_name のように、
アプリのあちこちで requests.get を直接呼んでいると、
テストのたびに patch のパスを考えたり、モックを組み立てたりする必要が出てきます。

これはだんだん苦しくなります。

良い方向性:外部APIクライアントを「クラス」として切り出す

外部APIとのやりとりを、専用のクラスに閉じ込めます。

import requests

class UserApiClient:
    def __init__(self, base_url: str) -> None:
        self._base_url = base_url

    def fetch_user_name(self, user_id: int) -> str:
        url = f"{self._base_url}/api/users/{user_id}"
        resp = requests.get(url, timeout=5)
        resp.raise_for_status()
        data = resp.json()
        return data["name"]
Python

アプリ本体は、このクライアントを受け取るようにします。

def greet_user(user_api: UserApiClient, user_id: int) -> str:
    name = user_api.fetch_user_name(user_id)
    return f"Hello, {name}!"
Python

テストでは、本物の UserApiClient の代わりに「モッククライアント」を渡せばよくなります。

class FakeUserApiClient:
    def fetch_user_name(self, user_id: int) -> str:
        return "Taro"

def test_greet_user_uses_name_from_api():
    fake_api = FakeUserApiClient()
    message = greet_user(fake_api, 1)
    assert message == "Hello, Taro!"
Python

ここでは、HTTPすら出てきません。
「APIクライアントのインターフェース」だけを前提にテストできています。

これが設計的にとても重要です。

外部API呼び出しを一箇所に集約する
アプリ本体は「インターフェース」に依存し、実装には依存しない

こうしておくと、モックが圧倒的に楽になります。


重要ポイント2:成功だけでなく「失敗パターン」をモックで再現する

本番でしか起きないエラーを、テストで再現できるようにする

APIモックの真価は、「エラーや異常系を自由に再現できる」ことです。

例えば、外部APIが 500 を返したときに、
アプリがどう振る舞うべきかをテストしたいとします。

UserApiClient に、エラー時に独自例外を投げるようにしておきます。

class UserApiError(Exception):
    pass

class UserApiClient:
    ...
    def fetch_user_name(self, user_id: int) -> str:
        url = f"{self._base_url}/api/users/{user_id}"
        resp = requests.get(url, timeout=5)
        if resp.status_code >= 500:
            raise UserApiError("server error")
        resp.raise_for_status()
        data = resp.json()
        return data["name"]
Python

これをモックでテストします。

from unittest.mock import patch, Mock
import pytest

def test_fetch_user_name_raises_on_server_error():
    fake_response = Mock()
    fake_response.status_code = 500
    fake_response.raise_for_status.return_value = None

    with patch("myapp.external.requests.get", return_value=fake_response):
        client = UserApiClient("https://example.com")
        with pytest.raises(UserApiError):
            client.fetch_user_name(1)
Python

本物のAPIに「わざと500を返して」と頼む必要はありません。
モックで好きなステータスコードを返せます。

タイムアウトも同様です。

from unittest.mock import patch
import requests
import pytest

def test_fetch_user_name_handles_timeout():
    with patch("myapp.external.requests.get", side_effect=requests.Timeout):
        client = UserApiClient("https://example.com")
        with pytest.raises(UserApiError):
            client.fetch_user_name(1)
Python

side_effect に例外を指定すると、
その関数が呼ばれたときに例外を投げてくれます。

これで、「本番でしか起きないようなエラー」を、
テスト環境で安全に再現できます。


重要ポイント3:「HTTPレベルのモック」と「クライアントレベルのモック」を使い分ける

HTTPレベルのモック(requests-mock など)

requests を直接モックする代わりに、
HTTPレベルでモックするライブラリもあります。

例えば responsesrequests-mock などです。

イメージだけ示すと、こんな感じです。

import requests
import responses

@responses.activate
def test_fetch_user_name_with_responses():
    responses.add(
        responses.GET,
        "https://example.com/api/users/1",
        json={"name": "Taro"},
        status=200,
    )

    name = fetch_user_name(1)
    assert name == "Taro"
Python

ここでは、

実際のHTTPは飛ばない
でも requests.get は普通に呼んでいるように見える
URLごとにレスポンスを定義できる

という形でモックできます。

HTTPクライアントの実装(requests の使い方)も含めてテストしたいときに有効です。

クライアントレベルのモック(インターフェースを差し替える)

一方で、アプリ本体のテストでは、
HTTPの細かい話は気にせず、「クライアントの振る舞い」だけモックしたいことが多いです。

その場合は、先ほどのように「Fakeクライアント」を渡す方がシンプルです。

HTTPレベルでモックするか
クライアントレベルでモックするか

は、「何をテストしたいか」で使い分けます。

HTTPクライアントの実装をテストしたいなら HTTPモック
アプリのビジネスロジックをテストしたいなら クライアントモック

というイメージです。


APIモックと設計・品質のつながり

モックしやすいコードは「依存が外から注入されている」

APIモックをやろうとすると、
自然と「依存性注入(DI)」っぽい設計になります。

外部APIクライアントをコンストラクタや引数で受け取る
アプリ本体はインターフェースにだけ依存する
テストでは、そのインターフェースを満たすフェイクを渡す

こうしておくと、

テストが書きやすい
外部APIの変更に強い
本番とテストで実装を差し替えやすい

というメリットが出てきます。

逆に、モックしづらいコードは、

あちこちで requests.get を直接呼んでいる
グローバルにクライアントを作っている
外部APIのURLやキーがコードにベタ書きされている

といった特徴があります。

「モックしやすさ」は、そのまま「設計の良さ」にかなり直結します。

APIモックがあると「自動テストの範囲」を広げられる

外部APIをモックできるようになると、

外部サービスが落ちていてもテストが回る
CI でも安心してテストを回せる
エラー系のテストケースを増やせる

という状態になります。

これは品質にとってかなり大きいです。

「成功パターンだけテストしている」状態から、
「失敗パターンも含めてテストしている」状態に進めるからです。


初心者がAPIモックを身につけるためのステップ

ステップ1:requestsを直接モックするテストを一つ書いてみる

unittest.mock.patch を使って、

requests.get を差し替える
モックレスポンスを返す
URLや引数が正しく呼ばれたか検証する

というテストを一つ書いてみる。

ここで「モックでHTTPを止める」感覚を掴む。

ステップ2:外部APIクライアントクラスを作り、Fakeクライアントでテストする

UserApiClient のようなクラスを作り、
アプリ本体はそのクライアントを受け取るようにする。

テストでは、HTTPを一切使わない FakeUserApiClient を渡してテストする。

ここで「インターフェースをモックする」感覚が掴める。

ステップ3:エラー系(500・タイムアウト)のテストをモックで書いてみる

side_effect やステータスコードを使って、
エラー時にどう振る舞うべきかをテストする。

ここまで行くと、「APIモック」が単なるテクニックではなく、
設計と品質の一部として見えてくるはずです。


まとめ(APIのモックは「外部サービスに頼らず、成功も失敗も自由に再現するための偽物づくり」)

初心者目線で整理すると、API のモックはこういうものです。

本物の外部APIをテストで毎回叩くのではなく、requests.get などをモックやフェイククライアントで差し替えて、「こう返ってくるはず」という前提でテストすることで、速く・安定して・エラー系まで含めてテストできるようにする技術。
外部APIクライアントをクラスとして切り出し、アプリ本体はそのインターフェースにだけ依存する設計にすると、モックが簡単になり、テストしやすさと設計の良さが同時に手に入る。
最初は unittest.mock.patchrequests.get を差し替えるところから始めて、次に「クライアントクラス+フェイク」、最後に「エラー系の再現」と段階を踏むと、APIモックが自分の武器として自然に使えるようになっていく。

タイトルとURLをコピーしました