最近のデプロイにより、すべてのユニットテスト、統合テスト、およびモックサーバーのチェックを通過していたにもかかわらず、3つのマイクロサービスが破損しました。チームはAPIモックを契約テスト(contract testing)に切り替えました。6ヶ月間で、契約数は3つから47つに増加し、月間の統合失敗率は2件から0件に減少しました。
なぜモックでは守れないのか
モックサーバーは、コンシューマー(利用者)が期待する形状を模倣するだけであり、プロバイダー(提供者)が実際にその形状を提供しているかどうかを確認することはありません。もしプロバイダーがフィールド名を(例えば name から display_name へ)変更した場合、モックは依然として古いペイロードを返し、コンシューマーのテストはパス(green)し続けますが、本番システムはクラッシュします。火曜日の午後2時に発生した本番環境の障害は、まさにこれでした。モックが実際の契約について「嘘をついた」のです。
契約テストがそのギャップを埋める
契約テストは、コードが本番環境に反映される前に、APIの両者が共有の定義に合意することを強制します。一般的なアプローチには以下の2つがあります。
- コンシューマー駆動契約 (Consumer-driven contracts) – コンシューミングサービスが期待値を記述し、プロバイディングサービスがそれを検証します。これは、共に進化する内部マイクロサービスに適しています。
- プロバイダー駆動契約 (Provider-driven contracts) – プロバイダーが仕様を公開し、コンシューマーがそれに対して自身のコードをチェックします。これはパブリックAPIにおける一般的なパターンです。
最初のアプローチは、一般的にマイクロサービスアーキテクチャ内での統合の破損を防ぎます。
コンシューマー駆動契約の仕組み
- コンシューマーが、プロバイダーから必要とするものを正確に記述したテストを作成します。
- テストを実行すると、それらの期待値を記録したJSONドキュメントである pact file が生成されます。
- プロバイダーは、CIパイプライン内で実際のサービスを pact file に対して実行します。
- プロバイダーがフィールドを変更した場合、検証が失敗し、ビルドがブロックされます。
検証は実際のプロバイダーコードに対して実行されるため、破壊的な変更はデプロイ後ではなく、早期に検知されます。
テストピラミッドにおける契約テストの位置付け
- ユニットテスト – 高速で、分離されたロジックをテストします。
- 契約テスト – 中速で、APIの合意が維持されていることを確認します。
- E2E(エンドツーエンド)テスト – 低速で、ビジネスフロー全体を実行します。
契約テストを、ユニットテストの迅速なフィードバックと、E2Eスイートの広範なカバレッジを繋ぐ架け橋として扱ってください。最も頻繁に破損する統合ポイントを対象とし、まずは2〜3個の重要なエンドポイントから始めてください。
実社会での導入事例
この記事のきっかけとなったチームは、最も脆弱な呼び出しをカバーする3つの契約から始めました。6ヶ月後には、サービス間通信の大部分をカバーする47の契約を保有していました。その期間中、APIの破損による障害は月2件から0件に減少しました。
契約テストが適さない場合
- すべてのサービスを単一のリポジトリで管理している個人開発者である場合。
- APIが非常に安定しており、数年間変更されていない場合。
- すぐに破棄される使い捨てのプロトタイプを構築している場合。
これらのシナリオでは、契約を維持するためのオーバーヘッドがメリットを上回る可能性があります。
潜在的なデメリットとその軽減策
- 契約を、それが記述するコードと並行してバージョン管理してください。
- 古い契約を避けるため、すべてのCI実行時に検証を自動化してください。
- 偶発的な破損をキャッチするために、プルリクエストで契約の変更をレビューしてください。
まとめ
もし、サービス間で通信ができることを確信するために、いまだに手作りのモックに頼っているとしたら、それは偽りの約束に賭けているようなものです。契約テストはその賭けを検証可能な合意へと変え、破壊的な変更が本番環境に到達する前に検知します。そして、このチームの数字が示すように、統合の失敗を完全に排除することも可能なのです。
