E2Eテスト:マイクロサービスアーキテクチャに必要な2つのレイヤー

Rui Li
E2Eテスト:マイクロサービスアーキテクチャに必要な2つのレイヤー カバー

マイクロサービスは境界で失敗します。通常はサービスの内部ではありません。内部の問題はユニットテストでほぼ検出できます。障害が発生するのは、サービスAがサービスBからの特定のレスポンス形式を期待しているにもかかわらず、サービスBを担うチームに知らせることなく誰かがフィールド名を変更した3スプリント前から、サービスBがその形式を提供しなくなっているといったケースです。

コントラクトテストは、このクラスの障害が本番環境に到達する前に検出するための手法です。分散システムにおいて最も活用されていないテストアプローチの一つでありながら、最も高い価値をもたらすものの一つです。完全なユーザー体験を検証する自律的なエンドツーエンドテストと組み合わせることで、特に効果を発揮します。

コントラクトテストとは

コントラクト(契約)とは、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コントラクトからブラウザまで包括的なカバレッジを実現できます。