参考文章:
聊一聊契约测试 —— ThoughtWorks洞见
契约测试
前后端分离了,然后呢?

契约测试全称为:消费者驱动契约测试,最早由 Martin Fowler 提出。契约这个词从字面上很容易理解,就是双方(多方)达成的共同协议,那又为什么需要契约测试这个东西呢?

在当前微服务大行其道的行业背景下,越来越多的团队采用了前后端分离和微服务架构,我们知道微服务是由单一程序构成的小服务,与其它服务使用 HTTP API 进行通讯,服务可以采用不同的编程语言与数据库,微服务解决了单体应用团队协作开发成本高、系统高可用性差等等问题。

但是微服务也引入了新的问题,假设 A 团队开发某服务并提供对应的 API,B 团队也在开发另一个服务,但是他们需要调用 A 团队的 API,为了产品的尽快发布,两个团队都争分夺秒,已经进入联调阶段了,然而出现了下面这样的尴尬情况。

随着越来越多的微服务加入,它们的调用关系开始变得越来越复杂,如果每次更改都需要和所有调用该 API 的团队协商,那沟通成本也未免太大了,试想下图的沟通成本。

为了保证 API 调用的准确性,我们会对外部系统的 API 进行测试,如果外部系统稳定性很差,或者请求时间很长的时候,就会导致我们的测试效率很低,当调用 API 失败时,你甚至无法确定是因为 API 被更改而导致的失败还是运行环境不稳定导致的失败。

A 团队提供的 API 不稳定,肯定会导致 B 团队效率低下,为了不影响 B 团队的进度,所以构建了测试替身,通过模拟外部 API 的响应行为来增强测试的稳定性和反应速度。

但是这样做真的就解决问题了吗?当所有内部测试都通过时,能拍着胸脯说真正的外部 API 就一定没有变化?很简单的一个解决方案就是:部分测试使用测试替身,另一部分测试定期使用真实的外部 API,这样既保证了测试的运行效率、调用端的准确性,又能确保当真实外部系统API改变时能得到反馈。

感觉剧情到这里就差不多该结束了,实际上真正的高潮部分开刚刚开始。如果外部 API 的反馈周期很长,那增加真实 API 测试间隔时间就又回到了最初的起点。现在我们回顾一下上面的方案。

在上面的场景中,我们都是已知外部 API 功能来编写相应的功能测试,并且使用直接调用外部 API 的方式来达到测试的目的,如此就不可避免的带来了两个问题:

  • API 调用者(消费者)对服务提供方(生产者)的更改是通过对 API 的测试来感知的;
  • 直接依赖于真实 API 的测试效果受限于 API 的稳定性和反映速度。

解决方案首先是依赖关系解耦,去掉直接对外部 API 的依赖,而是内部和外部系统都依赖于一个双方共同认可的约定—“契约”,并且约定内容的变化会被及时感知;其次,将系统之间的集成测试,转换为由契约生成的单元测试,例如通过契约描述的内容,构建测试替身。这样,同时契约替代外部 API 成为信息变更的载体。

前后照应一下,我们现在再来看一下消费者驱动契约测试。它有两个不可或缺的角色:消费者是服务使用方;生产者(提供者)是服务提供方。采用需求驱动(消费者驱动)的思想。契约文件(比如 json 文件)由双方共同定义规范,一般由消费者生成,生产者根据这份契约去实现。

契约测试其中一个的典型应用场景是内外部系统之间的测试,另一个典型的例子是前后端分离后的 API 测试。行业内比较成熟的解决方案是 Swagger SpecificationPact Specification,这里不做展开讨论。

我们同样可以把契约测试的思想用到代码的编写中,契约测试通过一个契约文件来解耦依赖,那么对于需要用户定义很多规则的场景,我们同样可以将这些规则像契约文件一样抽取出来,这样就降低了代码之间的耦合度。

最后敲敲黑板,契约测试不是替代 E2E 测试的终结者,更不是单元测试的升级换代,它更偏向于服务和服务之间的 API 测试,通过解耦服务依赖关系和单元测试来加快测试的运行效率。