取得コードのテストは書きにくい部類に入ります。相手が外にいるので、こちらの都合で状態を作れません。ネットワークが要るので遅く、相手が落ちていればテストも落ちます。テストが不安定だと、誰も結果を見なくなります。
かといって、応答をすべて手書きの作り物に置き換えると、今度は実物とのずれに気づけません。作り物は自分の思い込みどおりに動くので、思い込みが間違っていた場合のテストにならないのです。この回では、その折り合いをどう付けるかを扱います。
何を確かめたいのかを分ける
取得処理のテストは、確かめたいものが少なくとも3つに分かれます。ここを混ぜると、全部を1つのテストでやろうとして無理が出ます。応答の解釈が正しいかを見たいだけなのに、そのために本物の相手が必要になり、相手が落ちていると解釈のテストまで落ちる。原因の切り分けもできなくなります。
| 確かめたいこと | 本物のAPIが要るか | 手段 |
|---|---|---|
| 応答を正しく解釈できるか | 不要 | 記録した応答で |
| 失敗時に正しく振る舞うか | 不要 | で失敗を作る |
| 相手の仕様が変わっていないか | 必要 | を低頻度で |
上2つは本物を叩かずにできます。毎回のテストで叩く必要があるのは3つ目だけで、これは1日1回など低い頻度で構いません。開発中に何度も実行するテストと、仕様変更を見張るテストは、目的も適切な頻度も違います。混ぜると、遅くて不安定なテストが1つできあがるだけです。
本物の応答を一度記録して、使い回す
解釈のテストには、本物の応答を一度だけ取得して保存し、以降はそれを読む方式が向きます。と呼ばれる手法です。手書きの作り物と違い、実際に返ってきたものを使うので、思い込みが混ざりません。
保存した応答は としてリポジトリに置きます。ここで注意が要るのは、応答に含まれる個人情報や鍵をそのまま置かないことです。保存する前に該当する項目を伏せ字にします。一度コミットすると履歴から消えないので、置く前に確認してください。
記録する応答は、うまくいったものだけにしないでください。空の結果、1件だけの結果、項目が欠けている結果。こうした端の条件こそが壊れやすい場所です。実際に返ってきた変わった応答を見つけたら、それも記録に加えておくと、同じ壊れ方を二度としなくなります。
import jsonfrom pathlib import Path
FIXTURES = Path(__file__).parent / "fixtures"
def load_fixture(name: str): return json.loads((FIXTURES / f"{name}.json").read_text(encoding="utf-8"))
def test_parse_items(): """本物から一度だけ取って保存した応答を使う。ネットワークは要らない。""" payload = load_fixture("items_page1") items = parse_items(payload)
assert len(items) == 100 assert items[0]["id"] == "A-0001" # 数値として解釈できているか(文字列のままになっていないか) assert isinstance(items[0]["price"], (int, float)) # 欠損が None になっているか("" や "null" のままでないか) assert items[3]["name"] is None失敗の側をテストする
取得処理で本当に壊れるのは、成功したときではなく失敗したときです。429 が返ったら待つのか、500 なら何回やり直すのか、途中で切れたらどうなるのか。これらは本物の相手では再現できないので、作り物で状況を作ります。相手にわざと 500 を返してもらうことはできません。
class FakeResponse: def __init__(self, status_code, headers=None, payload=None): self.status_code = status_code self.headers = headers or {} self._payload = payload or {}
def json(self): return self._payload
def raise_for_status(self): if self.status_code >= 400: raise RuntimeError(f"HTTP {self.status_code}")
def test_retries_on_429(monkeypatch): """1回目は429、2回目は成功。待ち時間を計測して、待っていることを確かめる。""" calls = [] slept = []
def fake_get(url, **kwargs): calls.append(url) if len(calls) == 1: return FakeResponse(429, headers={"Retry-After": "2"}) return FakeResponse(200, payload={"items": []})
monkeypatch.setattr("mymodule.requests.get", fake_get) monkeypatch.setattr("mymodule.time.sleep", lambda s: slept.append(s))
fetch_with_retry("https://example.com/api")
assert len(calls) == 2 # 1回やり直している assert slept == [2.0] # Retry-After に従って待っているテストで本当に 2 秒待つと、テストが遅くなります。上のように待つ処理を差し替えて、待った長さだけを記録すれば、一瞬で検証できます。
相手が変わったことに気づく
記録した応答だけでテストしていると、相手の仕様が変わったことに永遠に気づけません。列が増えた、型が変わった、項目名が変わった。こうしたは、実際に叩いてみないと分かりません。
そこで、本物を叩くテストを別に用意し、低い頻度で回します。中身の値は変わるので、値ではなく形だけを確かめるのがこつです。件数がゼロでないか、必要な項目が存在するか、型が想定どおりか。
- 通常のテストとは分けて動かす — 失敗しても普段の開発を止めない
- 値ではなく形を見る — 「価格が 1200 円」ではなく「価格が数値である」
- 失敗したら通知する — 気づかれない検査は無いのと同じ
本物を叩くテストは、結果が安定しないことがあります。相手が一時的に落ちていた、たまたま結果がゼロ件だった、といった理由で失敗すると、になります。そうなると誰も結果を信じなくなるので、失敗したら即座に赤にするのではなく、数回続いたら通知するくらいの緩さで運用するほうが実際的です。
この形のテストと、記録した応答の更新をセットにしておくと、仕様変更に気づいたときの直しが早くなります。テストの設計そのものは LLM時代のテスト戦略 でも扱っています。次回からは、Python の道具の話に移ります。
この記事の感想を教えてください
あなたの 1 クリックで、本当にこの記事は更新されます。「もっと詳しく」「続編希望」が一定数集まった記事は、 ふくふくが 実際に内容を拡充したり続編記事を公開 します。 送信したリアクションはお使いのブラウザに記録され、再カウントされません。