Chuyển đến nội dung chính

第 22 課:契約測試 — Pact 和 API 相容性

使用 Pact 進行消費者驅動的合約測試。為什麼契約測試對微服務很重要。提供者驗證。契約經紀人。架構演進和向後相容性。整合到 CI/CD 管道中。

🏗️ 建築 — 第 22 課 第 22 課:契約測驗 — Pact 和 API 相容性

微服務與微前端系統設計-從基礎到生產

第 7 部分:測試全端微服務和微前端

亞洲開發網

簡介

當20個服務相互通訊時,如何確保更改服務A中的API不會破壞服務B? 合約測試解決了這個問題,而無需同時運行所有服務。


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:中央登錄 + can-i-deploy 安全檢查
  • CI/CD:在每個 PR 上運行,如果合約被破壞則阻止部署
  • 在整合測試之前捕獲重大變更**

下一篇文章: 第 23 課:Mono-Repo 與 Multi-Repo — 原始碼組織