APIの契約テスト:速いチームで破壊的変更を防ぐ
APIの契約テストは、Webやモバイルのリリース前に破壊的変更を検出します。実践的な手順、避けるべきミス、出荷前チェックリスト付き。

なぜ破壊的なAPI変更がリリースに紛れ込むのか
多くのチームでは、1つのAPIが複数のクライアントを提供します:Webアプリ、iOS、Android、場合によっては社内ツールも。エンドポイントは「同じ」でも、各クライアントはAPIを少しずつ違う使い方で頼っています。ある画面はあるフィールドが常に存在すると期待し、別の画面はフィルタが有効なときだけ使うかもしれません。
問題が顕在化するのは、こうしたコンポーネントが別々のスケジュールで出荷されるときです。バックエンドは1日に何度も本番に反映されることがあり、Webはすぐにデプロイできますが、モバイルはレビューサイクルや段階的ロールアウトのために遅れがちです。そのギャップが、思わぬ障害を生みます:APIは最新クライアント向けに更新されたが、昨日のモバイルビルドがまだ使われていて、対応できないレスポンスを受け取る状況です。
このときの症状は大抵わかりやすいです:
- フィールドがリネームや移動されたために画面が突然空になる
- 予期しないnullやオブジェクト欠如でのクラッシュ
- 再現が難しい「何かが壊れた」系のサポートチケット
- バックエンドデプロイ直後のエラーログの急増
- 根本原因を直さず防御的コードで対処するホットフィックスの多発
マニュアルテストやQAは、リスクの高いケースがハッピーパスに含まれないために見逃しがちです。テスターは「注文作成」が動くことを確認しても、古いアプリバージョン、途中まで埋められたプロフィール、稀なユーザーロール、リストが空のレスポンスなどを試さないかもしれません。キャッシュ、フィーチャーフラグ、段階的ロールアウトが加わると、テスト計画だけではカバーしきれない組み合わせが増えます。
典型例:バックエンドが status: "approved" をローカリゼーション対応のために status: { code: "approved" } に置き換えます。Webは同日更新され問題ありませんが、現在出回っているiOSのリリースはまだ文字列を期待していて、レスポンスの解析に失敗し、ログイン後に空白ページが表示されます。
これがAPIの契約テストの存在意義です:QAに取って代わるものではなく、「最新クライアント向けには動く」ような変更が本番に届く前に検出するための仕組みです。
契約テストとは(そして何ではないか)
契約テストは、APIの消費者(Web、モバイル、別のサービス)とAPIの提供者(バックエンド)が互いのやり取りについて合意する方法です。その合意が「契約」です。契約テストはひとつだけ確認します:提供者は依存する消費者の期待どおりに振る舞い続けているか?という点です。
実務では、契約テストはユニットテストとエンドツーエンドテストの中間に位置します。ユニットテストは速くローカルで動きますが、チーム間の境界での不一致を見逃しがちです(内部ロジックをテストするため)。エンドツーエンドは多くのシステムをまたいだ実際のフローを検証しますが、遅く、保守が難しく、テストデータやUIのタイミング、環境の不安定さなどAPI変更以外の理由で失敗することもあります。
契約は膨大なドキュメントではありません。消費者が送るリクエストと受け取るべきレスポンスの焦点を絞った記述です。良い契約は通常以下をカバーします:
- エンドポイントとメソッド(例:POST /orders)
- 必須・任意のフィールド、型、基本ルール
- ステータスコードとエラー時のレスポンス形(400と404の違いなど)
- ヘッダや認証の期待(トークンの存在、Content-Type)
- 重要なデフォルトや互換性ルール(フィールドが欠けたらどうなるか)
例えば契約テストが早期に検出する壊れ方の単純な例として、バックエンドが total_price を totalPrice にリネームした場合があります。ユニットテストは通るかもしれません。E2Eはその画面をカバーしていなかったり、後で不可解な失敗をするかもしれません。契約テストは即座に失敗し、どのフィールドが不一致かを指し示します。
契約テストが何をしないかも明確にしておきましょう。性能試験、セキュリティ試験、ユーザージャーニー全体のテストに取って代わるものではありませんし、すべてのロジックバグを検出するわけでもありません。実際に行うのは、速いチームで最も多いリスク、つまり「小さな」API変更がクライアントを黙って壊すことを減らすことです。
バックエンドが自動生成されるか頻繁に変更される(たとえば AppMaster のようなプラットフォームでAPIを再生成する場合)ときは、契約テストは実用的なセーフティネットになります。変更後にクライアントの期待が保たれていることを検証できるからです。
Webとモバイル向けに契約アプローチを選ぶ
Webとモバイルが頻繁にリリースされる場合、難しいのは「APIをテストすること」ではなく「各クライアントにとって変わってはならないことに合意すること」です。ここで契約テストは助けになりますが、契約の所有方法を選ぶ必要があります。
オプション1:消費者駆動契約(CDCs)
消費者駆動契約では、各クライアント(Web、iOS、Android、パートナー統合など)が自分が必要とするものを定義し、提供者がそれを満たせることを証明します。
クライアントが独立して動く場合に有効です。契約はバックエンドが「使われていると思っている」ものではなく、実際の使用方法を反映するからです。また、マルチクライアントの現実に合います:iOSはWebが使わないフィールドに依存することがあり、Webはモバイルが無視するソートやページングのルールを気にすることがあります。
単純な例:モバイルは price_cents が整数であることに依存しているが、Webは整形済みの表示だけを行うので、バックエンドが文字列に変えたらWebは気づきません。モバイル由来のCDCがその変更をリリース前に検出します。
オプション2:提供者が所有するスキーマ
提供者が1つの契約(スキーマや仕様)を公開してそれを強制し、消費者はその単一の真実に対してテストするパターンです。
多くの消費者を直接制御できない公開APIや、チーム間で厳格な一貫性が必要な場合に適しています。始めるのも簡単で、契約が1つ、レビューや変更の承認の流れが1つになります。
選び方の簡単な指標:
- クライアントが頻繁に出荷し、APIの異なる部分を使うならCDCを選ぶ。
- すべての人にとって安定した「公式」契約が必要なら提供者所有スキーマを選ぶ。
- 基本は提供者スキーマにして、リスクの高いエンドポイントだけCDCを併用するハイブリッドも有効。
AppMaster のようなプラットフォームで構築している場合も同じ考え方です:Webとネイティブモバイルを別々の消費者として扱ってください。共有のバックエンドであっても、依存するフィールドやルールが完全に一致することは稀です。
API契約に何を入れるべきか(実際に壊れを検出するために)
契約が役立つのは、それがWebやモバイルクライアントが本当に依存しているものを反映しているときだけです。誰も使わないきれいな仕様は、実際に本番を壊す変更を検出しません。
推測ではなく実際の利用から始めましょう。クライアントコード、APIゲートウェイのログ、チームからの短いリストなどから最も一般的な呼び出しを取り出し、それらを契約ケースに変換します:正確なパス、メソッド、ヘッダ、クエリパラメータ、典型的なリクエストボディの形。これで契約は小さく、関連性が高く、議論しにくくなります。
成功レスポンスだけでなく失敗レスポンスも含めてください。チームはハッピーパスだけをテストし、クライアントがエラーにも依存していることを忘れがちです:ステータスコード、エラーの形、安定したエラーコード/メッセージなど。モバイルアプリが特定の「メールは既に使われています」メッセージを表示しているなら、契約はその409レスポンスの形を固定化すべきです。
頻繁に壊れる領域には特に注意を:
- 任意フィールド vs 必須フィールド:フィールドを削除するより、任意フィールドを必須に変えるほうが危険。
- null:一部のクライアントは
nullと「存在しない」を異なるものとして扱う。許容範囲を決め一貫性を保つ。 - 列挙型(enum):新しい値を追加すると、閉じたリストを前提にした古いクライアントが壊れる可能性がある。
- ページング:パラメータとレスポンスフィールド(
cursorやnextPageTokenなど)を合意して安定化する。 - 日付や数値フォーマット:ISO文字列やセント単位の整数などを明示する。
契約の表現方法
チームが読みやすくツールで検証できるフォーマットを選びます。一般的な選択肢はJSON Schema、サンプルベースの契約、OpenAPIから生成した型モデルなどです。実務では、例(examples)とスキーマ検査を組み合わせると良いです:例は実際のペイロードを示し、スキーマは「フィールドのリネーム」や「型の変更」などを検出します。
簡単なルール:クライアントのアップデートを強制する変更なら契約テストは失敗するべきです。この考え方が契約を実際の破壊に焦点を当てさせます。
ステップバイステップ:CIパイプラインに契約テストを追加する
契約テストの目的は単純です:誰かがAPIを変更したとき、CIがその変更でWebやモバイルのクライアントが壊れるかどうかをリリース前に教えてくれることです。
1) クライアントが実際に依存しているものをまず記録する
単一のエンドポイントを選び、実際に重要な期待事項を書き出します:必須フィールド、フィールド型、許容値、ステータスコード、一般的なエラー応答。API全体を一度に記述しようとしないこと。モバイル向けには古いアプリバージョンの期待も含めてください。ユーザーはすぐにアップデートしません。
実践的なのは、今日クライアントが送っているいくつかの実リクエスト(ログやテストフィクスチャから)を取り、それらを再現可能な例にする方法です。
2) 契約をチームが管理する場所に置く
契約は忘れられたフォルダにあると失敗します。変更の近くに置きましょう:
- 両側を1つのチームが所有するなら、APIリポジトリに契約を保管する。
- Web、モバイル、APIが別チームなら、チームで共有するリポジトリに置く(個人のフォルダではなく)。
- 契約更新はコードと同様にレビューし、バージョン管理し、議論する。
3) CIの両側にチェックを追加する
2つのシグナルが欲しいです:
- 提供者検証(provider verification):各APIビルドで「APIは既知の契約を満たしているか?」を確認。
- 消費者チェック(consumer checks):各クライアントビルドで「このクライアントは最新公開契約と互換性があるか?」を確認。
これで両方向からの問題を捕まえられます。APIがレスポンスを変えるとAPI側のパイプラインが落ち、クライアントが新フィールドを期待し始めるとクライアント側のパイプラインが落ちます。
4) 失敗ルールを決めて運用する
マージやリリースを何がブロックするかを明確に決めてください。一般的なルールは:契約を破る変更はCIで失敗し、mainブランチへのマージをブロックする、というものです。例外が必要なら、調整されたリリース日などの書面による決定を要求します。
具体例:バックエンドが totalPrice を total_amount にリネームした場合、提供者検証が即座に失敗します。バックエンドチームは移行期間中に新旧両方のフィールドを返すことで安全に出荷を続けられます。
バージョニングと後方互換性を保ちながらチームを止めない方法
速いチームがAPIを壊す主原因は、既存のクライアントが依存しているものを変更してしまうことです。「破壊的変更」とは、以前は動作していたリクエストが失敗する、あるいはクライアントが扱えない形でレスポンスが意味合いを変えるような変更を指します。
よくある破壊的変更:
- クライアントが読んでいるレスポンスフィールドの削除
- フィールド型の変更(例:
"total": "12"から"total": 12) - 任意フィールドを必須にする(または新しく必須のリクエストフィールドを追加する)
- 認証ルールの変更(公開エンドポイントがトークン必須になるなど)
- ステータスコードやエラー形の変更(200から204へ、エラーフォーマットの変更など)
多くの場合、バージョンを上げずに回避できます。より多くのデータが必要ならフィールドを追加し、置き換えではなく付加で対応します。新しいエンドポイントが必要なら追加して古いものは維持します。検証を厳しくするなら古い入力と新しい入力の両方をしばらく受け入れ、徐々に新ルールへ移行します。契約テストはこれを促す役割を果たし、既存消費者が期待する動作が続くことを証明させます。
非互換性のない速度を維持するために、非推奨化(deprecation)が重要です。Webは日次で更新されるかもしれませんが、モバイルは審査や浸透の遅れで数週間遅れることがあります。非推奨化は希望ではなく実際のクライアントの挙動に合わせて計画してください。
現実的な非推奨ポリシー例:
- 変更を早めに告知する(リリースノート、社内チャネル、チケット)
- 使用量が合意した閾値を下回るまで旧挙動を維持する
- 非推奨経路が使われたらヘッダやログに警告を返す
- ほとんどのクライアントがアップグレードしたことを確認してから削除日時を設定する
- 削除は契約テストでアクティブな消費者がいないことを確認した後に行う
どうしても後方互換にできない根本的な変更(リソース形状やセキュリティモデルの大改変など)の場合にのみ明示的にバージョニングを使ってください。バージョニングは長期的コストを生みます:二つの挙動、二つのドキュメント、増えるエッジケースを維持する必要があります。バージョンは稀で意図的にし、契約で両バージョンが安全に動き続けることを確認しましょう。
よくある契約テストの失敗(と回避方法)
契約テストは、実際の期待をチェックしてこそ効果的です。トイシステムのようなテストだと、本番でバグは漏れます。失敗の多くは次の予測しやすいパターンから来ます。
ミス1:契約を「凝ったモック」のように扱う
過度なモッキングは古典的な罠です:契約テストは通るが、プロバイダの実装がその振る舞いを実際にできないためにデプロイ後に初めて失敗が露呈します。
安全なルールは単純です:契約は実行中の提供者(あるいは同様に振る舞うビルドアーティファクト)に対して検証しましょう。シリアライゼーションやバリデーション、認証ルールも実際のものを使って検証すべきです。
よくある間違いと対策:
- 過度なプロバイダモック:スタブではなく実際のプロバイダビルドで検証する。
- 契約を厳格にしすぎる:IDやタイムスタンプ、配列などは柔軟なマッチを使う。クライアントが依存しない全フィールドを必ず検証しない。
- エラー応答を無視する:主要なエラーケース(401, 403, 404, 409, 422, 500)とクライアントが解析するエラー本文の形をテストする。
- 所有権が不明瞭:要件変更時に誰が契約を更新するかを明確にし、API変更の「Definition of Done」に組み込む。
- モバイルの現実を忘れる:遅いネットワークや古いアプリバージョンを念頭にテストする。
ミス2:些細な変更でブロックする壊れやすい契約
新しい任意フィールドを追加しただけで契約が落ちる、JSONキーの順序が変わっただけで失敗する、という状態は開発者に赤いビルドを無視させてしまいます。
「重要なところは厳格に」を目指してください。必須フィールド、型、enum値、バリデーションルールは厳格に。余分なフィールドや順序、自然に変動する値には柔軟に対応する。
小さな例:バックエンドが status を "active" | "paused" から "active" | "paused" | "trial" に拡張したとします。モバイルが未知の値をクラッシュ扱いするならこれは破壊的変更です。契約は未知のenum値の扱いを検証するか、全クライアントが新値を扱えるまで提供者に既知の値のみ返すことを要求すべきです。
モバイルクライアントには特に注意を払ってください。長く野に出続けるため、ある変更が「安全」かどうかを判断する前に次を問うべきです:
- 古いアプリバージョンでもレスポンスをパースできるか?
- タイムアウト後に再試行されたリクエストはどうなるか?
- キャッシュされたデータは新フォーマットと衝突しないか?
- フィールドが欠けている場合のフォールバックはあるか?
APIが頻繁に生成・更新される場合(AppMasterを含む)、契約は実用的なガードレールになります。これにより、速く動きながらもWebとモバイルが各変更後も動作し続けることを証明できます。
出荷直前の簡単チェックリスト
API変更をマージ/リリースする直前に使うチェックリストです。Webとモバイルが頻繁に出荷されるときに最も大きな火災を起こす小さな編集を見つける目的で設計しています。すでに契約テストを導入している場合も、契約がブロックすべき破壊を見落としていないかにフォーカスするのに役立ちます。
毎回確認する5つの質問
- クライアントが読んでいるレスポンスフィールド(ネストされたものも含む)を追加・削除・リネームしていないか?
- ステータスコードは変わっていないか(200 vs 201、400 vs 422、404 vs 410)、あるいはエラーボディのフォーマットは変わっていないか?
- フィールドが必須と任意で入れ替わっていないか("null可" と "存在必須" の違いも含む)?
- ソート、ページング、デフォルトフィルタ(ページサイズ、並び順、カーソルトークン、デフォルト値)は変わっていないか?
- プロバイダとすべてのアクティブな消費者(Web、iOS、Android、社内ツール)の契約テストは実行されたか?
単純な例:APIが totalCount を返しており、クライアントがそれを「24結果」と表示しているとします。それを「リストにアイテムがあるから不要だ」として削除すると、バックエンドは問題なくてもUIは空白や「0件」と表示してしまうことがあります。エンドポイントが200を返していても、これは実際の破壊的変更です。
上の質問で「はい」があった場合のフォローアップ
出荷前に次を行ってください:
- 古いクライアントがアップデートなしで動くか確認する。動かないなら後方互換パスを追加する(旧フィールドを残す、両形式をサポートするなど)。
- クライアントのエラーハンドリングを確認する。多くのアプリは未知のエラー形を「何かが壊れた」として有益なメッセージを隠してしまう。
- サポート対象のすべてのリリース済みクライアントバージョンで消費者契約テストを実行する(最新ブランチだけでなく)。
内部ツール(管理パネルやサポート用ダッシュボードなど)を素早く構築する場合も、それらの消費者を含めるのを忘れないでください。AppMasterでは同じバックエンドモデルからWebとモバイルを生成することが多く、スキーマの小さな変更が出荷済みクライアントを壊すことをCIでの契約チェックなしに見落としがちです。
例:Webとモバイルが出荷する前に破壊的変更を検出する
一般的な構成を想像してください:APIチームは1日に何度もデプロイし、Webは日次で出荷し、モバイルは週次(アプリ審査や段階的ロールアウトのため)で出荷します。みんなが速く動いているので、リスクは悪意ではなく「取ると安全に見える小さな変更」です。
サポートチケットでユーザープロフィールのレスポンス名をわかりやすくしたいという要望が出ました。APIチームは GET /users/{id} のフィールド名を phone から mobileNumber にリネームしました。
このリネームは一見整理された変更ですが破壊的です。Webはプロフィールページで空白の電話番号を表示するかもしれません。モバイルは phone を必須と扱っているとクラッシュしたり、プロフィール保存時のバリデーションに失敗するかもしれません。
契約テストがあれば、これはユーザーに届く前に検出されます。実際の失敗の仕方はチェック方法によりますが典型的には:
- 提供者ビルドが失敗(API側): APIのCIが保存されたWeb・モバイルの消費者契約に対して検証を行い、消費者がまだ
phoneを期待しているのにプロバイダがmobileNumberしか返していないことを検出してビルドをブロックします。 - 消費者ビルドが失敗(クライアント側): Webチームが先に契約を
mobileNumber必須に更新してからAPIを出すと、消費者の契約テストがAPIがまだそのフィールドを返していないことを示して失敗します。
どちらの場合も失敗は早く、目立ち、具体的です:どのエンドポイントのどのフィールドが不一致かを指します。本番で「プロフィールページが壊れた」と騒ぐよりずっと良いです。
修正は通常シンプルで、破壊的でないように変更を加えることです。APIはしばらく新旧両方のフィールドを返します:
mobileNumberを追加する。phoneはエイリアスとして同じ値を保持する。- 契約の注記で
phoneを非推奨にする。 - Webとモバイルを
mobileNumberを参照するように更新する。 - サポートが確認されたら
phoneを削除する。
現実的なタイムライン例:
- 月曜 10:00: APIチームが
mobileNumberを追加し、phoneを残す。プロバイダ契約テストは通過。 - 月曜 16:00: Webが
mobileNumberに切り替えデプロイ。 - 木曜: モバイルが
mobileNumberに切り替えリリースを提出。 - 翌火曜: モバイルリリースが大部分のユーザーに行き渡る。
- 次スプリント: APIが
phoneを削除し、契約テストがサポートするクライアントがいないことを確認して完了。
契約テストはこのように「破壊的変更のルーレット」を管理された段階的な移行に変えます。
速く動くチームのための次のステップ(ノーコードの選択肢も含む)
契約テストが実際に破壊を防ぐためには、ローアウトを小さくし、所有権を明確にしてください。目標は単純です:Webとモバイルのリリースに届く前に破壊的変更を捕まえること。
軽量なローアウト計画から始めましょう。変更で最も痛みを生む上位3つのエンドポイント(通常は認証、ユーザープロフィール、コアな「リスト/検索」エンドポイント)を選び、まずそこを契約で保護します。チームがワークフローを信頼するようになったら範囲を広げます。
実践的な4週間ローアウト例:
- Week 1: 主要3エンドポイントの契約テストをPRごとに実行
- Week 2: モバイル使用量の多い次の5エンドポイントを追加
- Week 3: エラー応答やエッジケース(空の状態、バリデーションエラー)をカバー
- Week 4: 「契約がグリーン」をバックエンド変更のリリースゲートにする
次に、誰が何をするかを決めます。失敗の所有と変更承認が明確なときチームは速く動けます。
役割はシンプルに:
- 契約所有者:通常はバックエンドチーム。挙動変更時の契約更新責任を持つ。
- 消費者レビュアー:Web・モバイルのリードが変更が安全か確認する。
- ビルドシェリフ:日次または週次で回し、CIの契約失敗をトリアージする。
- リリースオーナー:契約が破られた場合にリリースを止める判断をする。
全員が気にする単一の成功指標を追いましょう。多くのチームにとって最良の指標は、リリース後のホットフィックスの減少と、API変更に起因するクライアント回帰(アプリクラッシュ、空白画面、チェックアウトの破損など)の減少です。
さらに速いフィードバックループを求めるなら、ノーコードプラットフォームは変更に伴う差異を減らし、再生成によりクリーンなコードを保つことでドリフトを抑えます。ロジックやデータモデルが変わったときに再生成すると、パッチの堆積で挙動が変わることを避けられます。
AppMasterでAPIとクライアントを構築しているなら、次の実践的ステップは今すぐ試してみることです:アプリを作成し、Data Designer(PostgreSQL)でデータをモデリングし、Business Process Editorでワークフローを更新してから、クラウドにデプロイ(またはソースコードをエクスポート)します。それをCI内の契約チェックと組み合わせれば、再生成されたビルドでもWebとモバイルが期待する挙動と一致していることを毎回証明できます。


