消費者主導の契約テストと Pact.io によるマイクロサービス統合テストの合理化
Ethan Miller
Product Engineer · Leapcell

分散システムの急速に進化する状況において、マイクロサービスはスケーラブルで回復力のあるアプリケーションを構築するための事実上のアーキテクチャとなりました。しかし、このアーキテクチャパラダイムは、独立してデプロイされたサービス間のシームレスな通信と互換性を確保するという重大な課題をもたらします。従来の統合テストでは、多くの場合、マイクロサービスクラスター全体をセットアップして実行する必要があります。これは、時間とリソースを大量に消費し、不安定になりがちです。このオーバーヘッドは、開発サイクルを大幅に遅らせ、継続的インテグレーションとデプロイメントを困難にします。この記事では、この問題に対処する強力な手法、すなわち消費者主導の契約テスト、特に Pact.io を使用することについて詳しく説明します。このアプローチにより、チームは、完全に起動された環境を必要とせずに、合意された契約を遵守するコンシューマーとプロバイダーを確保しながら、API 互換性を効率的に検証できます。
契約の力
Pact.io の詳細に入る前に、関連するコアコンセプトを明確に理解しましょう。
- マイクロサービス: アプリケーションを、疎結合された独立してデプロイ可能なサービスのコレクションとして構造化するアーキテクチャスタイル。各サービスは通常、特定のビジネス機能に焦点を当てています。
- API (アプリケーションプログラミングインターフェイス): 異なるソフトウェアアプリケーションが互いに通信できるようにする定義済みのルールのセット。マイクロサービスでは、API はサービス間通信の主要な手段です。
- 統合テスト: システムのさまざまなユニットまたはコンポーネント間の相互作用を検証するソフトウェアテストの一種。マイクロサービスのコンテキストでは、これは多くの場合、異なるサービスがどのように通信するかをテストすることを意味します。
- 消費者主導の契約テスト: API のコンシューマーがプロバイダーとの「契約」または期待される相互作用を定義するテスト方法。プロバイダーは、この契約を使用して、コンシューマーの期待を満たしていることを検証します。これにより、プロバイダー側の変更が、明示的な契約違反なしにコンシューマーを壊さないことが保証されます。
消費者主導の契約の基本原則は、従来のパワーダイナミクスを逆転させることです。プロバイダーが API を指示するのではなく、コンシューマーがそれを明示的に必要とするものを述べます。Pact.io は、このプロセスを促進する一般的なフレームワークです。
Pact.io の仕組み
Pact.io は、シンプルでありながら強力な 3 ステップのプロセスで動作します。
- コンシューマーテスト生成: コンシューマーサービスは、プロバイダーとの期待される相互作用を定義するテストを記述します。これらのテストは、プロバイダーへのリクエストをシミュレートし、期待されるレスポンスをアサートします。Pact.io は、これらの相互作用をキャプチャし、「Pact ファイル」—契約を表す JSON ドキュメント — を生成します。
- Pact ファイル発行: 生成された Pact ファイルは、多くの場合 Pact Broker と呼ばれる中央リポジトリに発行されます。このブローカーは、すべての契約の単一の真実の情報源として機能します。
- プロバイダー検証: プロバイダーサービスは、Pact Broker から関連する契約を取得し、実際の API 実装がこれらの契約に準拠していることを検証します。この検証プロセスは、プロバイダーのビルドパイプラインの一部として実行されます。
これは、簡略化された例で説明しましょう。OrderService (コンシューマー) と ProductCatalogService (プロバイダー) という 2 つのサービスを想像してみてください。OrderService は ProductCatalogService から製品詳細を取得する必要があります。
コンシューマーコード例 (JavaScript、Pact.js を使用)
// order-service/tests/contract/product-catalog.spec.js const { pact, provider } = require('@pact-foundation/pact'); const ProductServiceClient = require('../../src/ProductServiceClient'); // 当社のコンシューマークライアント describe('ProductCatalogService Integration', () => { let client; beforeAll(() => { // Pact モックサーバーを設定 return provider.setup(); }); afterEach(() => provider.verify()); afterAll(() => provider.finalize()); it('should be able to get a product by ID', async () => { const expectedBody = { id: 'P123', name: 'Laptop', price: 1200.00 }; await provider.addInteraction({ uponReceiving: 'a request for a product by ID', withRequest: { method: 'GET', path: '/products/P123', headers: { 'Accept': 'application/json' }, }, willRespondWith: { status: 200, headers: { 'Content-Type': 'application/json' }, body: expectedBody, }, }); client = new ProductServiceClient(provider.mockService.baseUrl); const product = await client.getProductById('P123'); expect(product).toEqual(expectedBody); }); });
このコンシューマーテストでは、
@pact-foundation/pactを使用してモックプロバイダーを設定しています。provider.addInteractionは契約を定義します。コンシューマーが/products/P123にGETリクエストを送信すると、モックサーバーは特定の JSON ボディとステータスで応答する必要があります。ProductServiceClientは、このモックサーバーに実際のリクエストを行い、そのレスポンスがアサートされます。- テストが完了すると、Pact.js は
pactsディレクトリにproduct-catalog-service-order-service.jsonファイルを生成します。
プロバイダーコード例 (Spring Boot、Pact JVM を使用)
// product-catalog-service/src/test/java/com/example/ProductCatalogServicePactVerificationTest.java package com.example; import au.com.dius.pact.provider.junit5.HttpTestTarget; import au.com.dius.pact.provider.junit5.PactVerificationContext; import au.com.dius.pact.provider.junit5.PactVerificationProvider; import au.com.dius.pact.provider.junitsupport.Provider; import au.com.dius.pact.provider.junitsupport.loader.PactBroker; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; @Provider("ProductCatalogService") // 当社のプロバイダー名 @PactBroker(host = "localhost", port = "9292") // Pact Broker の場所 @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @ExtendWith(PactVerificationProvider.class) public class ProductCatalogServicePactVerificationTest { @LocalServerPort private int port; @BeforeEach void setUp(PactVerificationContext context) { context.setTarget(new HttpTestTarget("localhost", port)); } @TestTemplate void pactVerificationTest(PactVerificationContext context) { context.verifyInteraction(); } }
プロバイダーテストでは:
@Provider("ProductCatalogService")はこのサービスをプロバイダーとして識別します。@PactBrokerは契約を取得する場所を指定します。@SpringBootTest(webEnvironment = SpringTest.WebEnvironment.RANDOM_PORT)は実際のスプリングブートアプリケーションをランダムポートで起動します。setUpメソッドは Pact をこの実行中のインスタンスに向くように構成します。context.verifyInteraction()は、Pact にブローカーから契約を取得させ、これらの契約に基づいて実際の実行中のProductCatalogServiceへの実際の要求を行い、レスポンスが定義された契約と一致することをアサートするように指示します。
これにより、OrderService 自体を実行する必要なく、プロバイダーがコンシューマーの期待に対してテストできる方法が示されます。
アプリケーションシナリオとメリット
消費者主導の契約テストは、いくつかのシナリオで威力を発揮します。
- マイクロサービスエコシステム: 主なユースケースであり、互換性を維持しながら、サービスの独立した開発とデプロイメントを可能にします。
- API ゲートウェイ: ゲートウェイがサービス契約に従ってリクエストを正しくルーティングおよび変換していることを確認します。
- サードパーティ統合: 外部 API との契約を定義して、壊れた変更を早期に検出しますが、多くの場合、外部当事者は検証に Pact.io を採用しない可能性があります。
主なメリットは次のとおりです。
- 破壊的変更の早期検出: 契約違反はプロバイダーのビルドパイプラインで検出され、問題が本番環境に到達するのを防ぎます。
- 統合テストの複雑さの軽減: 互換性テストのためにマイクロサービスクラスター全体を起動する必要がなくなります。時間とリソースを節約します。
- より高速なフィードバックループ: 開発者は API 変更に対する即時フィードバックを得て、開発を加速します。
- 明確な API コミュニケーション: 契約は、チーム間の API 期待を明示的に定義する生きたドキュメントとして機能します。
- 信頼と協力の向上: チームが互換性が確保されていることを知って、自信を持って変更を行える協力的な環境を育みます。
結論
Pact.io による消費者主導の契約テストは、マイクロサービスアーキテクチャにおける API 互換性を管理するためのエレガントで効率的なソリューションを提供します。期待値の定義の責任をコンシューマーにシフトし、これらの期待値を提供者のビルドパイプラインで検証することにより、チームは従来の統合テストに関連する複雑さと不安定さを大幅に軽減できます。このアプローチは、より高速な開発サイクル、より堅牢なデプロイメント、そして全体的なシステムに対するより高い信頼につながります。消費者主導の契約を採用して、より回復力がありスケーラブルなマイクロサービスエコシステムを構築してください。

