Skip to content
返回质量保障

契约测试

验证消费者的期望和提供者的响应一致,无需跑完整集成。

为什么

在微服务或第三方 API 世界里,每个消费-提供对的完整集成测试代价过高——你需要所有服务一起跑。契约测试用轻量交换替代:消费者记录它期望的响应形状(契约),提供者验证它始终产生该形状。这在提供者侧抓住破坏性变更,在任何消费者受影响之前,且每条契约毫秒级而非每条完整集成分钟级。Pact 和 Spring Cloud Contract 是成熟工具;模式分提供者驱动(OpenAPI)和消费者驱动(Pact)。

何时使用

当你有多个服务或第三方 API 依赖时。在第一次破坏性变更咬你之前就建好消费者驱动契约——给已有集成补契约很痛苦,因为每个端点已经漂移了。

如何做

1. 选方向:消费者驱动(Pact)或提供者驱动(OpenAPI)
   - 消费者驱动:消费者写期望,提供者对照验证
   - 提供者驱动:提供者发布 OpenAPI spec,消费者生成 stub

2.(消费者驱动)写消费者 pact 测试
   - 在消费者测试套件中定义请求和期望响应
   - 测试对 mock 提供者跑并生成 pact JSON 文件
   - 这个文件就是契约——提供者将对照它验证

3. 把 pact 发布到 broker(Pact Broker 或 Pactflow)
   - broker 存契约,标消费者版本和环境
   - 它追踪哪些契约已被哪些提供者版本验证

4.(提供者)在提供者 CI 中验证 pact
   - 把每个 pact 请求对真实提供者端点回放
   - 若响应形状不匹配 pact,构建失败
   - 把验证结果发回 broker

5. 用"can-i-deploy"检查门控部署
   - 部署消费者前问 broker:"生产中的提供者版本已验证我的契约吗?"若否,阻止部署

示例

// 消费者: tests/orders-api.pact.test.ts (Pact JS)
import { Pact } from "@pact-foundation/pact";
import path from "path";

const pact = new Pact({
  consumer: "web-frontend",
  provider: "orders-api",
  dir: path.resolve(__dirname, "pacts"),
});

describe("Orders API 契约", () => {
  beforeAll(() => pact.setup());
  afterAll(() => pact.finalize());

  it("按 id 返回订单", async () => {
    await pact.addInteraction({
      uponReceiving: "请求一个订单",
      withRequest: { method: "GET", path: "/orders/123" },
      willRespondWith: {
        status: 200,
        headers: { "Content-Type": "application/json" },
        body: { id: 123, status: "shipped", total: 49.99 },
      },
    });
    const order = await fetchOrder(123); // 消费者真实客户端调用
    expect(order).toEqual({ id: 123, status: "shipped", total: 49.99 });
  });
});

// 提供者: CI 中验证 pact
// npx pact-verifier --pact-url=./pacts/web-frontend-orders-api.json
//   --provider-base-url=http://localhost:8080 --publish-verification-results

常见问题

  • ⚠只验快乐路径(200 响应)的契约漏掉每个错误形状变更——提供者开始返回 422 带不同错误体,消费者的错误处理静默坏掉。
  • ⚠发布 pact 但提供者侧从不跑验证给出虚假信心——契约存在但没人查提供者是否真遵守它。
  • ⚠不给 pact 标消费者部署环境意味着 can-i-deploy 检查无法区分"对 staging 验证"和"对生产验证",破坏性变更到生产。
  • ⚠过度指定契约(每个字段、每个嵌套对象、精确字符串值)使其如此脆弱,任何无害新增可选字段都挂验证。

最佳实践

  • ✓在 pact 体中用匹配器(like、eachLike、term)而非精确值——断言形状和类型而非具体数据,使契约扛得住合理数据变更。
  • ✓每个端点至少含一条错误路径契约(缺失 404、无效输入 422)使消费者的错误处理也被协议覆盖。
  • ✓把 can-i-deploy 检查接入部署流水线作硬门控——若目标环境中的提供者版本未验证消费者契约,消费者不应部署。
  • ✓每次提供者 CI 运行把验证结果发到 broker,使仪表盘始终显示所有消费-提供对的当前兼容状态。