在前面几篇文章中,我们学习了怎么对一个微服务,实施从单元到整体的全部测试。下一步,我们就需要考虑怎么测试不同微服务之间的协同、交互。如果采用传统的总体测试方法,对服务之间的协作进行验证,那么随着服务数量和调用关系复杂度的增加,必须面临成本呈现指数级增长的挑战,这表现在:

  • 验证成本高:为了验证多个服务协作后的功能正确与否,需要为每个服务搭建基础设施(包括其依赖的数据库、缓存等),并执行部署、配置等步骤,以确保服务能正确运行。

  • 结果不稳定:微服务构建的系统本质上是分布式系统,服务间通信通常都是跨网络调用的。当对服务间协作进行测试时,网络延迟、超时、带宽等因素都会影响到测试结果,极易导致结果不稳定。

  • 反馈周期长:相比于传统的整体式(Monolithic)应用,微服务架构下的可独立部署单元多,因此集成测试的反馈周期比传统的方式更长,定位问题所花费的时间也更长。

因此,如何提升微服务间协同测试的有效性,成了服务规模化后必须要面对的挑战。契约测试可以帮助我们在简化测试流程的同时,提高测试的覆盖率。这是微服务架构下一种特别典型的测试方法,我们下面将详细加以介绍。

什么是契约测试

在这里,契约(Contract)是指服务消费者(Consumer)与提供者(Provider)之间协作的规约。契约通常包括:

  • 请求:指消费者发出的请求。包括请求头(Header)、请求内容(URI、Path、HTTP Verb)和请求参数等。
  • 响应:指提供者应答的响应。可能包括响应的状态码(Status Word)、响应体的内容(XML/JSON) 或者错误的信息描述等。
  • 元数据:指对消费者与提供者间一次协作过程的描述。譬如消费者/提供者的名称、上下文及场景描述等。

契约测试(Contract Test),就是基于契约,对消费者与提供者间协作的验证。通过契约测试,我们就能将契约作为中间的标准,验证提供者提供的内容是否满足消费者的期望。契约测试分两种类型,一种是消费者驱动,一种是提供者驱动。其中最常用的,是消费者驱动的契约测试(Consumer-Driven Contract Test,简称 CDC)。核心思想是从消费者业务实现的角度出发,由消费者端定义需要的数据格式以及交互细节,生成一份契约文件。然后生产者根据契约文件来实现自己的逻辑,并在持续集成环境中持续验证该实现结果是否正确。

image

如下图所示,当消费者与提供者之间建立契约(v0)后,如果提供者提供的内容被意外修改(譬如从 v0 变化成v1),则提供者的 v1 版本显然无法满足之前定义的契约(v0),这样契约测试用例就会失败,从而及时发现提供者接口变化导致的错误,并对其进行修正。

image

CDC 的核心流程包括如下两步:

  1. 对消费者的业务逻辑进行验证时,先对其期望的响应做模拟提供者(Mock);并将请求(消费者)-响应(基于模拟提供者)的协作过程,记录为契约;
  2. 通过契约,对提供者进行回放,保证提供者所提供的内容满足消费者的期望。

CDC 有几个核心原则:

  • CDC 是以消费者提出接口契约,交由提供者实现,并以测试用例对契约进行产生约束,所以提供者在满足测试用例的情况下,可以自行更改接口或架构实现方法,而不影响消费者。
  • CDC 是一种针对外部服务接口进行的测试,它能够验证服务是否满足消费者期待的契约。它的本质是从利益相关者的目标和动机出发,最大限度地满足需求方的业务价值实现。实际上,CDC 和前几年出现的 TDD(测试驱动开发)、BDD(行为驱动开发)的思路如出一辙。
  • 契约测试不是组件测试(单服务测试),并不需要深入地检查微服务的功能,而是只检查微服务请求的输入、输出是否包含了必要的数据结构和属性,以及响应延时、速度等是否在预期的范围之内。

虽然契约测试可以帮助消费者一侧的服务开发团队确认协作没问题,它对于提供者一端的开发团队也很有帮助,因为他们在开发过程中,可以通过契约测试结果确认自己的改动,不会对其他的相关服务产生不利的影响。

在开发团队设计一个新服务时,CDC 也非常有用。开发人员可以通过一系列契约测试用例,界定他们需要从该服务获得的响应,从而决定 API 的设计方法。

如何设计契约测试

下面用一个实际的例子说明设计契约测试的方法。这个例子中,一个微服务提供了一个包含三个字段(“ID”、“name”和“age”)的资源,供三个消费者微服务使用。这三个微服务分别使用这个资源中的不同部分。消费者 A 使用其中的 ID 和 name 这两个字段。因此,测试脚本中将只验证来自提供者的资源中是否正确包含这两个字段,而不需要验证 age 字段。消费者 B 使用 ID 和 age 字段,而不需要验证 name 字段。消费者 C 则需要确认资源中包含了所有这三个字段。

image

现在,如果提供者需要将 name 分为姓(first name)和名(last name),那么就需要去掉原有的 name 字段,加入新的 first name 字段和 last name 字段。这时执行契约测试,就会发现消费者 A 和 C 的测试用例就会失败。测试用例 B 则不受影响。这意味着消费者 A 和 C 服务的代码需要修改,以兼容更新之后的提供者。修改之后,还需要对契约内容进行更新。

这里涉及到一个重要的设计原理“伯斯塔尔法则”(Postel's law,又称鲁棒性法则):Be conservative in what you send, be liberal in what you accept"。

中文可以译作:严于律己,宽于律人。即提供者在供给资源时,要非常严格地按照规范执行。而消费者在接收资源时,应当只关注自己需要的信息,而尽可能宽容地处理自己不需要的信息或者无关的错误(类似于上例中消费者 B 的契约测试用例不会因为无关字段的变化而失败)。

目前,业界常用的 CDC 测试框架有:

其中应用最为广泛的是 Pact,本课将主要以 Pact 为例,说明契约测试的设计方法。

Pact 是实现 CDC 的框架之一,最早由 REA 公司(一家澳大利亚房产门户网站),为克服在微服务演进过程中面临的服务间测试问题而开发。Pact 主要支持服务间 RESTful 接口的验证,经过几年的发展,Pact 已经提供了 Ruby、JVM/Scala、JS、Swift 等多个版本。最近几年,随着微服务的快速发展,很多知名软件公司都开始使用 Pact,构建微服务的测试体系,例如 SoundCloud、Redhat、Pivotal Labs、ThoughtWorks 等。

Pact 的工作流程简单来说主要分为两步:

1.基于消费者的业务逻辑,生成契约文件。

image

实现步骤具体为:

  • 使用 Pact 的 DSL,模拟作为提供者的服务。
  • 消费者对模拟提供者发送请求。
  • 使用 Pact 的 DSL,定义响应(包括 Headers、Status word 以及 Body 等)。
  • 使用 @PactVerification 运行单元测试(Pact 集成了 JUnit、RSpec 等框架)。

下面提供一个例子,使用基于 Junit 的 Pact DSL 定义响应内容,并支持了两个测试用例:

  • 然后,在消费者端执行该 Junit 测试,就可以生成契约文件,保存为 JSON 格式,其中包含了消费者的名称、发送的请求、期望的响应以及元数据。对于上面这个例子,执行:

成功执行后,你就可以在 Pacts\Wang 下面找到所有测试生成的契约文件。

到此,契约就生成了。我们可以将其保存在文件系统中,或者保存在 Pact-Broker(Pact 提供的用来管理契约文件的中间件)中,以便后续提供者使用。将契约文件上传到 Broker 服务器非常简单:

2.用消费者生成的契约对提供者进行验证。

image

在提供者端,我们不需要再写任何验证的代码,因为 Pact 已经提供了验证接口,我们只需要做好如下配置:

  • 为提供者指定契约文件的存储源(如文件系统或者 Pact-Broker)。
  • 启动提供者。
  • 运行 PactVerify(Pact 有 Maven、Gradle 或者 Rake 插件,提供 pactVerify 命令)。

当执行 pactVerify 时,Pact 将按照如下步骤,自动完成对提供者的验证:

  • 构建 Mock 的消费者。
  • 根据契约文件记录的请求内容,向提供者发送请求。
  • 从提供者获取响应结果。
  • 验证提供者的响应结果与 Pact 契约文件定义的契约中是否一致。

传统情况下做多个服务的集成测试时,需要把服务消费者和服务提供者两个服务都启动起来再进行测试,而 Pact 做契约测试时将它分成两步来做,每一步里面都不需要同时启动两个服务。这是 Pact 最强大的地方,此外它还有其他一些特性:

  • 测试解耦,就是服务消费端与提供端之间解耦(Decoupling),甚至可以在没有提供者实现的情况下开始消费端的测试。
  • 一致性,通过测试保证契约与现实是一致性的。
  • 测试前移,可以在开发阶段运行,并作为连续集成的一部分,甚至在开发本地就可以去做,而且可以看到一条命令就可以完成,便于尽早发现问题,降低解决问题的成本。
  • Pact 提供的 Pact Broker 可以自动生成一个服务调用关系图,为团队提供了全局的服务依赖关系图,如下图所示。
  • Pact 提供 Pact Broker 这个工具来完成契约文件管理,使用 Pact Broker 后,契约上传与验证都可以通过命令完成,且契约文件可以制定版本。
  • 使用 Pact 这类框架,能有效帮助团队降低服务间的集成测试成本,尽早验证当提供者接口被修改时,是否破坏了消费端预期的数据格式。

image

本课总结

契约测试可以帮助我们验证微服务之间的协同和交互。通过将精力集中于检查消费者和使用者之间的契约,可以大幅降低测试成本和提高测试效率。本课着重介绍了契约测试的概念、步骤,并以 Pact 为例介绍了其实现方法。

在完成了上述课程之后,下一步将从外部用户的角度,检查整个系统的功能是否符合预期,这就要用前端的端到端测试(End to End test)。我们将在下一课中详细介绍。

参考文献

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐