最近的一次部署导致三个微服务崩溃,尽管所有的单元测试、集成测试和 mock-server 检查都通过了。团队将 API mocks 替换为契约测试(contract testing)。在六个月内,契约数量从 3 个增加到 47 个,每月集成失败率从两次事故降至零。

为什么 Mock 无法保护你

Mock 服务器仅模拟消费者预期的结构;它从不检查提供者(provider)是否真的提供了该结构。如果提供者重命名了一个字段——例如从 name 改为 display_name——Mock 仍然返回旧的负载(payload),消费者的测试依然显示为绿色(通过),而生产系统则会崩溃。周二下午 2 点发生的生产故障正是如此:Mock 对真实的契约“撒了谎”。

契约测试填补了这一空白

契约测试强制 API 的双方在任何代码进入生产环境之前,就一个共享定义达成一致。目前有两种常见的方法:

  • 消费者驱动契约 (Consumer-driven contracts) – 由消费服务编写预期;由提供服务进行验证。这非常适用于共同演进的内部微服务。
  • 提供者驱动契约 (Provider-driven contracts) – 由提供者发布规范;消费者根据该规范检查其代码。这是公共 API 的常用模式。

第一种方法通常可以防止微服务架构内部的集成破坏。

消费者驱动契约的工作原理

  1. 消费者编写一个测试,准确描述其对提供者的需求。
  2. 运行测试会生成一个 pact file —— 一个记录这些预期的 JSON 文档。
  3. 提供者在其 CI 流水线中针对 pact file 运行其真实服务。
  4. 如果提供者更改了字段,验证将失败,构建将被拦截。

由于验证是在真实的提供者代码上运行的,因此任何破坏性变更都能被及早发现,而不是在部署后才发现。

契约测试在测试金字塔中的位置

  • 单元测试 (Unit tests) – 速度快,测试隔离的逻辑。
  • 契约测试 (Contract tests) – 中等速度,确认 API 协议依然有效。
  • 端到端测试 (End-to-end tests) – 速度慢,执行完整的业务流程。

将契约测试视为单元测试的快速反馈与端到端测试套件的广泛覆盖之间的桥梁。针对最常发生故障的集成点,从两三个关键端点开始。

现实世界的采用案例

撰写本文灵感的团队最初只编写了三个契约,涵盖了他们最脆弱的调用。六个月后,他们拥有了 47 个契约,涵盖了大部分服务间流量。在此期间,API 破坏事件从每月两次降至零。

何时契约测试可能不值得

  • 你是一名单人开发者,将所有服务都维护在单个代码库中。
  • API 非常稳定,多年未曾更改。
  • 你正在构建一个很快就会被丢弃的临时原型。

在这些场景下,维护契约的开销可能会超过其带来的收益。

潜在的缺点及缓解措施

  • 将契约与它们所描述的代码进行版本管理。
  • 在每次 CI 运行中自动化验证,以避免契约过时。
  • 在 pull requests 中审查契约变更,以捕捉意外的破坏。

总结

如果你仍然依赖手工编写的 mocks 来让自己相信服务可以正常通信,那么你是在赌一个虚假的承诺。契约测试将这种赌博变成了一份可验证的协议,在破坏性变更到达生产环境之前将其拦截。正如该团队的数据所示,它甚至可以完全消除集成失败。