はじめに
20 個のサービスが相互に通信する場合、サービス A の API を変更してもサービス B が中断されないようにするにはどうすればよいでしょうか? Contract Testing は、すべてのサービスを同時に実行することなく、この問題を解決します。
1. 問題: API の重大な変更
Service A (Consumer) gọi Service B (Provider):
Service B thay đổi response:
Before: { "price": 999 } ← number
After: { "price": "999.00" } ← string!
→ Service A parse Price as number → crash!
→ Integration test phát hiện? Chỉ nếu chạy cả A + B cùng lúc
→ E2E test? Chậm, flaky, expensive
→ Contract test? ✅ Phát hiện sớm, CI tốc độ cao
2. 消費者主導の契約テスト
2.1 概念
Consumer (Order Service):
"Tôi expect response từ Product Service có format:
{ id: string, name: string, price: number }"
→ Ghi thành Pact (contract file)
Provider (Product Service):
"Để tôi verify rằng API của tôi satisfy contract này"
→ Run Pact verification against real API
Nếu fail → Provider biết thay đổi sẽ break Consumer
→ Fix API hoặc coordinate migration
2.2 フロー
┌──────────────┐ 1. Generate ┌──────────────┐
│ Consumer │────Pact File───►│ Pact Broker │
│ (Order Svc) │ (contract) │ (central │
└──────────────┘ │ registry) │
└──────┬───────┘
│
2. Verify│
▼
┌──────────────┐
│ Provider │
│ (Product Svc)│
└──────────────┘
3. 協定の履行
3.1 消費者側 (注文サービス)
// order-service/tests/pact/product.consumer.test.js
const { PactV3 } = require('@pact-foundation/pact');
describe('Product Service Contract', () => {
const provider = new PactV3({
consumer: 'OrderService',
provider: 'ProductService',
});
it('get product by id', async () => {
// Define expected interaction
provider
.given('product 123 exists')
.uponReceiving('a request for product 123')
.withRequest({
method: 'GET',
path: '/api/products/123',
})
.willRespondWith({
status: 200,
body: {
id: '123',
name: like('Laptop'),
price: like(999),
inStock: like(true),
},
});
// Execute test against Pact mock
await provider.executeTest(async (mockService) => {
const product = await fetchProduct(mockService.url, '123');
expect(product.id).toBe('123');
expect(typeof product.price).toBe('number');
});
});
});
3.2 プロバイダーの検証 (製品サービス)
// product-service/tests/pact/provider.verify.test.js
const { Verifier } = require('@pact-foundation/pact');
describe('Pact Verification', () => {
it('validates OrderService contract', async () => {
const verifier = new Verifier({
providerBaseUrl: 'http://localhost:8080',
pactBrokerUrl: 'https://pact-broker.company.com',
provider: 'ProductService',
providerVersion: process.env.GIT_SHA,
publishVerificationResult: true,
stateHandlers: {
'product 123 exists': async () => {
await seedDB({ id: '123', name: 'Laptop', price: 999 });
},
},
});
await verifier.verifyProvider();
});
});
4. CI/CD の統合
# Consumer CI (Order Service)
consumer-contract-test:
steps:
- run: npm test -- --testPathPattern=pact
- run: npx pact-broker publish pacts/
--consumer-app-version=$GIT_SHA
--branch=$BRANCH
# Provider CI (Product Service)
provider-contract-verify:
steps:
- run: npm run pact:verify
# can-i-deploy check before deploymentt
- run: npx pact-broker can-i-deploy
--pacticipant=ProductService
--version=$GIT_SHA
--to-environment=production
4.1 デプロイできるか
Before deploying Product Service v2.1:
$ pact-broker can-i-deploy \
--pacticipant ProductService \
--version 2.1 \
--to production
✅ OrderService (1.5) → ProductService (2.1): VERIFIED
✅ CartService (2.0) → ProductService (2.1): VERIFIED
Result: Safe to deploy!
5. スキーマ進化のベストプラクティス
| タイプの変更 | 壊れる? | 戦略 |
|---|---|---|
| フィールドを追加 | いいえ | 追加するだけです (消費者は不明を無視します) |
| フィールドを削除する | はい | 非推奨にする → 消費者が使用していないことを確認する → 削除 |
| フィールドの名前を変更 | はい | 新しい追加 → コンシューマの移行 → 古い |
| タイプの変更 | はい | 新しいフィールド → 移行 → 古いフィールドを削除 |
| エンドポイントを追加 | いいえ | |
| エンドポイントを削除 | はい | 契約テストで使用状況を把握 |
概要
- コントラクト テスト = サービス間の API コントラクトを検証する
- 消費者主導: 消費者が期待を定義し、プロバイダーが検証します
- Pact: 業界標準ツール、多くの言語をサポート
- Pact Broker: 中央レジストリ + 安全性チェックを導入可能
- CI/CD: すべての PR で実行され、契約が違反した場合はデプロイをブロックします
- 統合テスト前に重大な変更をキャッチします