E2Eテスト:すべてのマイクロサービスアーキテクチャに必要な2つの層

マイクロサービスは境界部分で障害を起こします。通常、サービス内部では起きません――その大部分はユニットテストで検出されます。障害が発生するのは、サービスAがサービスBからのレスポンスの形式を期待しているにもかかわらず、サービスBが3スプリント前にフィールド名を変更した際、サービスAのオーナーチームに知らせることなくその提供を止めてしまった場合です。
コントラクトテストは、この種の障害が本番環境に到達する前に検出するための手法です。分散システムにおいて最も活用されていないテストアプローチの一つでありながら、最も高い価値をもたらすものの一つでもあります――特に、完全なユーザーエクスペリエンスを検証する自律的なエンドツーエンドテストと組み合わせた場合はなおさらです。
コントラクトテストとは
コントラクト(契約)とは、2つのサービス間のやり取りを正式に定義したものです。コンシューマーが受け取ることを期待する内容と、プロバイダーが提供することに合意した内容を規定します。コントラクトテストは、両サービスを同時に起動することなく、それぞれが独立してコントラクトを遵守していることを検証します。
これがインテグレーションテストとの重要な違いです。インテグレーションテストは、ライブ環境で2つのサービスが連携して動作することを検証します。コントラクトテストは、各サービスが独立した状態で定められた責務を果たしていることを検証します。コンシューマー側のテストは、コンシューマーのコードが合意済みのレスポンス形式を正しく処理することを確認します。プロバイダー側のテストは、プロバイダーが実際にその形式を返すことを確認します。
テストが独立して実行されるため、高速で信頼性が高く、複雑な環境セットアップも不要です。また、同じコントラクト定義から派生しているため、コンシューマーとプロバイダーのどちらが進化しても、両者の同期が保証されます。
マイクロサービスにこれが特に必要な理由
モノリシックなアプリケーションは、コンパイラによって型が強制される関数呼び出しを通じて内部で通信します。共有データ構造のフィールド名を変更すると、すべての呼び出し箇所がコンパイル時に壊れます――インターフェースの不一致をリリースすることはできません。
マイクロサービスはHTTPまたはメッセージングプロトコルを介して通信しており、コンパイル時の強制はありません。サービスAはuserIdフィールドを期待するコードでデプロイされ、サービスBはuser_idを返すようになったコードでデプロイされることがあります。両サービスは個別には正常に動作しています。しかし、連携は壊れています。ユニットテストではこれを検出できません。ステージング環境でのインテグレーションテストで検出できる場合もありますが、ステージングは本番のトポロジーを十分に正確に再現できないことが多く、すべてのケースを検出できるわけではありません。
コントラクトテストはこのギャップを埋めます。サービスBがレスポンスの形式を変更すると、プロバイダーのコントラクトテストは即座に失敗します――変更がデプロイされる前に――そしてサービスBのオーナーチームは、本番環境で何かが壊れる前にサービスAのチームに通知する必要があることを把握できます。
コンシューマー駆動コントラクト
最も効果的なコントラクトテストモデルはコンシューマー駆動型です。コンシューマーサービスがプロバイダーへの期待を定義し、プロバイダーは自身のテストスイートの一部としてそれらの期待に対して検証を行います。
これは通常のダイナミクスを逆転させます。プロバイダーがAPIを定義してコンシューマーが正しく使用することを期待するのではなく、コンシューマーが要件を定義し、プロバイダーがそれを満たしていることを検証します。APIの進化は、下流に影響を与える一方的な変更ではなく、交渉の場となります。
Pactなどのツールがこのモデルを実装しています。コンシューマーテストがコントラクトのアーティファクトを生成します。プロバイダーテストはそのアーティファクトに対して実行されます。コントラクトの両サイドが独立してテストされ、コントラクトはコードと並行してバージョン管理されます。
コントラクトテストとリアルなユーザーエクスペリエンスのギャップ
コントラクトテストは強力ですが、特定の制限があります。データの形式やAPIの動作を検証するものであり、ユーザー向けの結果を検証するものではありません。コントラクトテストは、決済APIが正しいJSON構造を返すことを確認します。しかし、チェックアウトフローが実際に機能するかどうか――ユーザーがカートに商品を追加し、配送情報を入力し、決済を処理し、確認画面を見られるかどうか――は確認しません。
ユーザー向けの検証には、実際のユーザーが行うようにアプリケーションスタック全体を操作するエンドツーエンドテストが必要です。自律型E2Eテストエージェントは、製品要件とコードベースを読み取り、完全なユーザーフローを検証するテストを生成・実行します――APIコントラクトだけでなく、ユーザーがブラウザで実際に体験する動作を検証します。
この2つのアプローチは補完的な関係にあり、それぞれ異なる障害モードをカバーします。
コントラクトテストはサービス間のインターフェースの不一致を検出します――配管層です。バックエンドチームがAPIフィールド名を変更したりレスポンス形式を変更したりすると、コントラクトテストはサービスレベルで即座に失敗します。迅速なフィードバックと的確な診断が可能です。
E2Eテストはフルスタック全体のユーザー向けリグレッションを検出します――エクスペリエンス層です。フロントエンドの変更がチェックアウトフローを壊す場合、認証の更新がユーザーをロックアウトする場合、APIの変更が正しいデータを生成しているにもかかわらずUIが誤ってレンダリングする場合などを検出します。これらはユーザーが実際に報告するバグです。
CIで両方の層を自動化する
コントラクトテストはサービスレベルのCIに組み込み、すべてのPRで実行されるべきです。既存のコンシューマーコントラクトを破壊するプロバイダーサービスへの変更は、プロバイダーのパイプラインを失敗させるべきです――変更が共有環境に到達する前に、チームへ即座なフィードバックを提供します。
E2EテストはアプリケーションレベルのCIに組み込み、同様にすべてのPRで実行されるべきです。自律型テストエージェントがフルスイートを生成・実行します――UIフロー、API統合、セキュリティチェック、認証――数分で、コントラクトテストでは検出できないユーザー向けリグレッションを検出します。
両方の層をCIで実行するチームは、最も早い段階で障害を検出できます。サービス境界でのコントラクト違反と、アプリケーション境界でのユーザーエクスペリエンスリグレッションです。どちらの層だけでも十分ではありません。両方を組み合わせることで、APIコントラクトからブラウザまでの包括的なカバレッジが得られます。