API 101 专栏 · 第 84

API 开发中的契约测试:确保可靠集成

2026年01月16日
API 开发中的契约测试:确保可靠集成

关键要点

  • 左移集成验证:契约测试通过验证消费者期望与提供者能力匹配,在开发早期 捕获集成故障,防止代价高昂的"在隔离环境中工作,在生产环境中失败"的场景。
  • 独立部署信心:契约作为团队之间的 版本化规范,使提供者能够演进其 API,消费者能够独立部署,同时通过自动化验证保持兼容性保证。
  • 消费者驱动的方法:与传统的以提供者为中心的 API 规范不同,消费者驱动的契约 捕获实际使用模式,确保 API 服务于真实需求并防止影响实际消费者的破坏性变更。
  • 互补测试策略:契约测试填补了单元测试(太孤立)和端到端测试(太慢且脆弱)之间的关键空白,在不需要完全部署环境的情况下提供对集成点的快速、集中反馈。

什么是契约测试?

在应用程序通过 API 通信的分布式系统中,最持久和代价高昂的问题之一是 集成故障。后端服务通过了所有单元测试,前端应用程序针对 模拟 API 完美工作,但在一起部署时,由于对请求格式、响应结构或行为的期望不匹配,集成会中断。

契约测试 是一种测试方法论,旨在通过明确定义和验证 API 消费者和提供者之间的"契约"来解决这个特定问题。契约是一个正式的、可执行的规范,描述:

  • 什么请求 消费者将发出(HTTP 方法、路径、请求头、请求体结构)
  • 什么响应 提供者将返回(状态码、请求头、请求体结构)
  • 在什么条件下(特定请求触发特定响应)

与通过部署两个服务并运行端到端测试来测试完整集成不同,契约测试根据约定的契约 独立 验证每一方。消费者测试验证"我发送的请求与契约匹配",而提供者测试确认"我根据契约响应"。如果双方都满足契约,集成就会工作。

1flowchart TD
2    subgraph Consumer_Side["消费者方(前端/服务 A)"]
3        C[消费者代码] --> CM[消费者测试]
4        CM -->|生成| CC[消费者契约]
5    end
6
7    subgraph Contract_Broker["契约代理(Pact Broker)"]
8        CC -->|发布| CB[(存储的契约)]
9    end
10
11    subgraph Provider_Side["提供者方(后端/服务 B)"]
12        CB -->|检索| PC[提供者契约]
13        PC --> PM[提供者测试]
14        PM -->|验证| P[提供者代码]
15    end
16
17    PM -->|验证结果| CB
18
19    style CC fill:#e3f2fd,stroke:#1976d2
20    style CB fill:#f3e5f5,stroke:#7b1fa2
21    style PC fill:#e3f2fd,stroke:#1976d2
22    style CM fill:#fff3e0,stroke:#f57c00
23    style PM fill:#fff3e0,stroke:#f57c00

契约与传统 API 规范

区分契约与传统 API 文档很重要:

方面传统 API 规范(OpenAPI)消费者驱动的契约(Pact)
作者提供者团队定义能力消费者团队定义需求
范围完整的 API 表面(所有端点、所有可能的响应)仅消费者实际使用的特定 API 交互
演进以提供者为中心:"这是我们提供的"以消费者为中心:"这是我们需要的"
验证通常是手动的或松散执行的在 CI/CD 中自动验证双方
目的文档和代码生成防止破坏性变更和实现独立部署

两种方法都很有价值且 互补。OpenAPI 规范记录你完整的 API 表面,而消费者驱动的契约验证真实集成工作。

为什么契约测试对现代 API 开发至关重要

当你了解它在真实世界开发工作流中解决的问题时,契约测试的案例变得令人信服。

1. 在部署前防止集成故障

传统的测试金字塔依赖端到端(E2E)测试来捕获集成问题。然而,E2E 测试 缓慢、脆弱且昂贵

  • 它们需要完全部署的测试环境,所有服务都在运行
  • 它们很慢(几分钟到几小时),提供延迟的反馈
  • 它们因与你的代码无关的原因而失败(网络问题、测试数据问题、外部服务不可用)
  • 随着架构的增长,维护成本很高

契约测试在几秒钟内提供 快速、集中的集成验证,作为常规单元测试套件的一部分运行。契约测试失败会立即告诉你"此更改将中断与服务 X 的集成",甚至在你推送到共享环境之前。

真实世界示例:一家电子商务公司有一个移动应用程序(消费者)和一个产品目录 API(提供者)。后端团队向产品创建端点添加了一个必需的 category_id 字段并更新了他们的文档。然而,移动团队的代码不包括这个字段。没有契约测试,这只会在部署新后端并且移动应用程序开始在生产环境中失败时才被发现。使用契约测试,提供者针对现有消费者契约的验证会在其 CI/CD 管道中立即失败,从而防止部署。

2. 实现独立部署和微服务自治

在微服务架构中,关键承诺之一是 独立可部署性——团队可以在不与其他团队协调的情况下部署其服务。然而,当集成中断直到运行时才被发现时,这个承诺就会崩溃。

契约测试通过使契约成为服务之间的 版本化接口 来恢复这种自治。在部署提供者服务的新版本之前,你可以根据所有发布的消费者契约进行验证。如果验证通过,你就有很高的信心部署不会中断现有消费者。

同样,消费者团队可以部署新版本,只要他们遵守契约,提供者就会正确地为他们服务。这创建了一个 可靠的部署安全网,实现真正的组织可扩展性。

3. 消费者驱动的设计带来更好的 API

传统的 API 设计通常遵循以提供者为中心的方法:"我们应该公开什么能力?"消费者驱动的契约测试通过捕获 实际消费者需求 来翻转这一点。每个契约代表一个真实的使用模式,回答"消费者实际上需要从这个 API 得到什么?"

这具有强大的影响:

  • 防止过度工程:你只构建消费者实际使用的东西,而不是假设的功能。
  • 识别未使用的端点:如果端点没有消费者契约存在,可能可以安全地弃用。
  • 指导版本控制策略:契约准确显示哪些消费者会受到提议的破坏性变更的影响,从而能够对弃用时间表和向后兼容性做出数据驱动的决策。

4. 永不过时的文档

契约是 活的、可执行的文档。与不可避免地偏离现实的书面文档不同,契约会持续验证。如果 API 行为在不更新契约的情况下发生变化,测试就会失败。这提供了对 API 做什么以及消费者如何使用它的始终准确的参考。

如何实施契约测试:实用指南

实施契约测试需要选择正确的工具、建立工作流并将验证集成到 CI/CD 管道中。

步骤 1:选择你的契约测试方法

有两种主要范式:

消费者驱动的契约测试(CDCT):消费者根据其需求定义契约。主导工具是 Pact

  • 最适合:你控制消费者和提供者的微服务架构
  • 优势:捕获真实使用,实现双向验证

基于规范的契约测试:共享规范(OpenAPI、GraphQL 模式)充当契约。像 DreddSchemathesisPortman 这样的工具验证实现与规范匹配。

  • 最适合:公共 API,使用 OpenAPI 进行 API 优先设计的团队
  • 优势:单一事实来源,与现有 API 规范配合使用

在本指南中,我们将重点介绍 Pact,因为它是采用最广泛的 CDCT 工具,但这些原则广泛适用。

步骤 2:使用 Pact 实施消费者端契约测试

在消费者端,你编写定义与提供者的预期交互的测试。Pact 捕获这些交互并生成契约文件。

示例:消费者测试(Node.js 与 Pact)

1// consumer.pact.test.js
2const { PactV3 } = require('@pact-foundation/pact');
3const { API } = require('../api-client');
4
5// 定义提供者
6const provider = new PactV3({
7  consumer: 'ProductCatalogUI',
8  provider: 'ProductAPIService',
9  dir: './pacts',
10});
11
12describe('Product API Contract', () => {
13  describe('GET /products/:id', () => {
14    it('returns product details for valid ID', async () => {
15      // 定义预期交互
16      await provider
17        .given('product with ID prod-123 exists')  // 提供者状态
18        .uponReceiving('a request for product prod-123')
19        .withRequest({
20          method: 'GET',
21          path: '/products/prod-123',
22          headers: {
23            'Authorization': 'Bearer token-placeholder',
24            'Accept': 'application/json'
25          }
26        })
27        .willRespondWith({
28          status: 200,
29          headers: { 'Content-Type': 'application/json' },
30          body: {
31            id: 'prod-123',
32            name: 'Laptop',
33            price: 999.99,
34            in_stock: true
35          }
36        });
37
38      // 执行测试
39      await provider.executeTest(async (mockServer) => {
40        const api = new API(mockServer.url);
41        const product = await api.getProduct('prod-123');
42
43        expect(product.id).toBe('prod-123');
44        expect(product.name).toBe('Laptop');
45        expect(product.price).toBe(999.99);
46      });
47    });
48
49    it('returns 404 for non-existent product', async () => {
50      await provider
51        .given('product with ID invalid does not exist')
52        .uponReceiving('a request for non-existent product')
53        .withRequest({
54          method: 'GET',
55          path: '/products/invalid',
56          headers: { 'Accept': 'application/json' }
57        })
58        .willRespondWith({
59          status: 404,
60          headers: { 'Content-Type': 'application/json' },
61          body: {
62            error: 'Product not found'
63          }
64        });
65
66      await provider.executeTest(async (mockServer) => {
67        const api = new API(mockServer.url);
68        await expect(api.getProduct('invalid')).rejects.toThrow('Product not found');
69      });
70    });
71  });
72});

当这些测试运行时,Pact 生成一个 契约文件ProductCatalogUI-ProductAPIService.json),描述预期的交互。然后将此文件 发布到 Pact Broker,一个契约的集中存储库。

步骤 3:将契约发布到 Broker

Pact Broker 对于协调团队之间的契约至关重要。它存储契约、跟踪验证结果并提供部署安全网。

1# 将消费者契约发布到 broker
2npx pact-broker publish ./pacts \
3  --consumer-app-version=$(git rev-parse HEAD) \
4  --branch=main \
5  --broker-base-url=https://pact-broker.your-company.com \
6  --broker-token=$PACT_BROKER_TOKEN

步骤 4:实施提供者端契约验证

在提供者端,你从 broker 检索契约并验证你的 API 实现满足它们。

示例:提供者验证(Node.js 与 Pact)

1// provider.pact.test.js
2const { Verifier } = require('@pact-foundation/pact');
3const app = require('../app');  // 你的 Express/Fastify 应用
4
5describe('Pact Verification', () => {
6  it('validates the provider against consumer contracts', async () => {
7    const server = app.listen(3000);
8
9    try {
10      await new Verifier({
11        provider: 'ProductAPIService',
12        providerBaseUrl: 'http://localhost:3000',
13
14        // 从 broker 获取契约
15        pactBrokerUrl: 'https://pact-broker.your-company.com',
16        pactBrokerToken: process.env.PACT_BROKER_TOKEN,
17
18        // 针对所有消费者的主分支进行验证
19        consumerVersionSelectors: [
20          { mainBranch: true },
21          { deployedOrReleased: true }
22        ],
23
24        // 提供者状态设置
25        stateHandlers: {
26          'product with ID prod-123 exists': () => {
27            // 设置测试数据:确保产品存在于测试数据库中
28            return database.seed({
29              id: 'prod-123',
30              name: 'Laptop',
31              price: 999.99,
32              in_stock: true
33            });
34          },
35          'product with ID invalid does not exist': () => {
36            // 确保产品不存在
37            return database.remove('invalid');
38          }
39        },
40
41        // 发布验证结果
42        publishVerificationResult: true,
43        providerVersion: process.env.GIT_COMMIT
44      }).verifyProvider();
45
46    } finally {
47      server.close();
48    }
49  });
50});

此验证测试:

  1. 启动你的实际 API 服务
  2. 从 broker 检索所有相关消费者契约
  3. 根据你的真实 API 重放契约中定义的每个交互
  4. 验证响应与期望匹配
  5. 将结果发布回 broker

步骤 5:集成到 CI/CD 管道

当集成到持续集成工作流中时,契约测试会提供最大价值。

消费者管道

1# .github/workflows/consumer-ci.yml
2name: Consumer CI
3on: [push, pull_request]
4jobs:
5  test:
6    runs-on: ubuntu-latest
7    steps:
8      - uses: actions/checkout@v2
9      - uses: actions/setup-node@v2
10
11      - name: Install dependencies
12        run: npm ci
13
14      - name: Run consumer contract tests
15        run: npm run test:pact
16
17      - name: Publish contracts to broker
18        if: github.ref == 'refs/heads/main'
19        run: |
20          npx pact-broker publish ./pacts \
21            --consumer-app-version=${{ github.sha }} \
22            --branch=main
23        env:
24          PACT_BROKER_TOKEN: ${{ secrets.PACT_BROKER_TOKEN }}

提供者管道

1# .github/workflows/provider-ci.yml
2name: Provider CI
3on: [push, pull_request]
4jobs:
5  test:
6    runs-on: ubuntu-latest
7    steps:
8      - uses: actions/checkout@v2
9      - uses: actions/setup-node@v2
10
11      - name: Install dependencies
12        run: npm ci
13
14      - name: Run unit tests
15        run: npm test
16
17      - name: Verify provider against consumer contracts
18        run: npm run test:pact:verify
19        env:
20          PACT_BROKER_TOKEN: ${{ secrets.PACT_BROKER_TOKEN }}
21          GIT_COMMIT: ${{ github.sha }}
22
23      - name: Can I deploy?
24        run: npx pact-broker can-i-deploy \
25          --pacticipant=ProductAPIService \
26          --version=${{ github.sha }} \
27          --to-environment=production
28        env:
29          PACT_BROKER_TOKEN: ${{ secrets.PACT_BROKER_TOKEN }}

can-i-deploy 命令至关重要:它在允许部署之前检查是否验证了所有消费者契约,提供 部署安全网

1sequenceDiagram
2    participant Dev as 开发者
3    participant CI as CI/CD 管道
4    participant Broker as Pact Broker
5    participant Prod as 生产环境
6
7    Dev->>CI: 推送代码更改
8    CI->>CI: 运行单元测试
9
10    alt 消费者更改
11        CI->>CI: 运行消费者契约测试
12        CI->>Broker: 发布新契约
13        Broker-->>CI: 契约已存储
14    end
15
16    alt 提供者更改
17        CI->>Broker: 获取消费者契约
18        Broker-->>CI: 返回契约
19        CI->>CI: 根据契约验证提供者
20        CI->>Broker: 发布验证结果
21    end
22
23    CI->>Broker: 我可以部署到生产环境吗?
24    Broker->>Broker: 检查所有契约已验证
25
26    alt 所有契约有效
27        Broker-->>CI: ✅ 可以安全部署
28        CI->>Prod: 部署新版本
29    else 未验证的契约
30        Broker-->>CI: ❌ 部署被阻止
31        CI-->>Dev: 构建失败:契约违规
32    end

高级契约测试模式和最佳实践

1. 处理提供者状态

提供者状态(given('product with ID prod-123 exists'))对于真实测试至关重要。最佳实践:

  • 使用测试数据库:为每个状态在测试数据库中播种特定数据
  • 在测试之间重置状态:确保每次验证从干净的状态开始
  • 保持状态最小:仅设置特定交互所需的数据

2. 版本控制和向后兼容性

契约自然会演变。优雅地处理变更:

  • 非破坏性变更:向响应添加可选字段是安全的——消费者会忽略未知字段
  • 破坏性变更:需要协调。在进行更改之前使用 broker 识别所有受影响的消费者
  • API 版本控制:当需要破坏性变更时,对你的 API 进行版本控制(/v2/products)并创建单独的契约

3. 使用 API 网关的契约测试

对于使用像 Apache APISIX 这样的 API 网关 的团队,契约测试提供额外的好处:

  • 网关配置验证:将网关路由规则、认证和速率限制策略视为契约的一部分。验证网关转换不会破坏消费者期望。
  • 集中式契约执行:网关可以作为所有后端服务的提供者验证点,集中契约验证。

示例:测试 APISIX 网关配置

1// 验证 APISIX 网关正确代理到后端
2await new Verifier({
3  provider: 'ProductAPIGateway',
4  providerBaseUrl: 'http://localhost:9080',  // APISIX 网关
5  // ... 其余验证配置
6});

这确保网关转换(请求头修改、请求/响应转换)不会违反消费者契约。

4. 与其他测试策略结合

契约测试 不是 其他测试类型的替代品——它是互补的:

  • 单元测试:在隔离环境中验证单个组件
  • 契约测试:验证集成点和 API 契约
  • 负载测试:验证压力下的性能
  • 端到端测试:验证关键业务工作流(谨慎使用)

理想的测试金字塔有广泛的单元测试基础、大量的契约测试中间层和狭窄的 E2E 测试顶部。

结语

契约测试代表了我们在分布式系统中处理集成测试方式的 根本转变。通过使服务之间的契约明确、版本化和自动验证,它提供了现代开发团队快速移动而不破坏事物所需的快速反馈和部署信心。

消费者驱动契约的力量在于其 捕获真实使用模式 的能力,使提供者能够在完全了解谁会受到变更影响的情况下演进其 API。与用于协调的 Pact Broker 结合并集成到 CI/CD 管道中,契约测试创建了一个强大的安全网,实现真正的独立可部署性——微服务架构的圣杯。

对于构建 API 驱动系统的组织,特别是那些使用像 Apache APISIX 这样的 API 网关 进行集中控制的组织,契约测试应该是测试策略的不可协商部分。它填补了单元测试(太孤立)和端到端测试(太慢且脆弱)之间的关键空白,以正确的速度提供正确级别的集成信心。

设置契约测试基础设施的投资——选择工具、建立工作流、集成到 CI/CD——会立即在减少集成故障、加快开发周期和增加对每次部署的信心方面产生回报。

下一步

请继续关注我们即将推出的 API 101 专栏,你将在其中找到最新的更新和见解!

渴望深化你对 API 网关的了解?关注我们的 Linkedin,获取直接发送到你收件箱的宝贵见解!

如果你有任何问题或需要进一步的帮助,请随时联系 API7 专家

微信咨询

获取方案