微服务pact契约测试

微服务集成测试

迁移到微服务对测试我们的系统产生了新的挑战。理论上每个微服务都应该是隔离的并可以独立操作。但在实践中一个服务如果没有其他部分通常没什么用。另一方面,为一个服务拉起整个系统的拓扑进行测试抵消了微服务期望带来的模块化和封装。

挑战在于如何检验与其他服务集成后没有问题。我们希望越早越好。而且我们不想将复杂的生产环境重现一遍。一般来说这种检验是集成功能测试或叫端到端测试。但实际是当我们的系统越来越复杂,端到端带来的收益越少。 大量的相互依赖导致误报和很长的执行周期。 使得测试变得很难管理与调试。

这甚至有一个测试金字塔理论(最初由 Mike Cohn 在他的著作 ‘Succeeding with Agile’ 中提到)讲述了为了优化你的投入,你需要更少的高层次的端到端测试,写更多的低层次的单元测试。

22_微服务pact契约测试.png

单元测试很好!但在它带来的所有收益中 - 他们对测试与其他服务的集成没什么作用。

那我们怎样保证每个服务团队可以独立的迭代但又能保证整体系统的健康呢?我们如何实现持续交付,小批量生产,快速反馈,而又不会在每次变更时引起服务出问题呢?

一个可能的答案是 Consumer-Driven Contract(CDC) 测试。这种测试策略是基于一种多年前就定义的服务进化模式。它现在分布式系统变得更常见后变得更适合了。

Consumer-Driven Contracts

我尝试简单解释一下。 Consumer-Driven Contracts 实际就是面向服务与服务关系的合约。意思就是不想以前是 provider 提供方定义接口与服务级别是什么样(同事消费者 consumer 尽量适配) - 现在消费者来领舞。

每个消费者来定义它期望服务提供方需要交付与需要检查的。这就将集成的责任转移到服务提供方。

那就变成以下流程:

23_微服务pact契约测试.png

在商务合约上者通常描述成 ‘将消费者放在第一位’ 或 ‘倾听你的客户’。因为想要提供最好的服务我们需要尽量做到客户期望和需要的。而不是我们假设对的事。

当讨论微服务进化时 - 在那种每个服务都有一个独立团队开发的大型企业里尤其重要。有时这些团队也可能在不同的地理位置和区域。这影响了即时沟通和让业务功能进化更有挑战性。

合约测试框架

消费者驱动合约当然可以通过投资团队间的沟通与协作来管理。 也可以通过使用结构化的系列化格式如 protobuf,thrift 或 messagepack 消息体来解决。但如果要管理一个定义好的流程 - 最好使用框架,尤其如果是个开源的。

这种框架已经出现了。这其中最杰出和活跃的是 Pact 和 Spring Cloud Contract。后者只针对使用 JVM 的项目。 而 Pact 使用 Ruby 写的但可以支持很多语言,包括 JavaGoPythonJavascript。 让它很适合在复杂,多样性的微服务系统中使用。

今天我们会看看如何在两个服务间定义和校验合约。消费者服务是用 Python 写的。而提供方服务是用 Go 写的。测试会在我们的 CI/CD 流程中进行 - 也就是在 Codefresh 流水线里面。

Pact

Pact是什么

Pact 是一个用于实现消费者驱动的契约测试的框架。系统工程里有这样一个规律,线性系统(即复杂性随规模线性增长的系统)的可靠性等于组成它的各个组件的可靠性之乘积。这容易理解,因为整个系统正常工作的条件是必须每个组件都同时正常工作。

24_微服务pact契约测试.png

也就是说,如果一个系统由三个可靠性各 90% 的组件组成,那么整个系统的可靠性实际只有 90%×90%×90%=72.9%,低于任一组件的可靠性。如果一个系统由 100 个组件组成,每个组件即使能达到 99% 的可靠性,那么整个系统的可靠性也会降到 36.6% 左右。

然而,一个复杂软件系统的情形可能更糟,因为软件系统实际上是非线性系统。组成软件系统的各个组件之间有依赖与连接关系,除了要让每个组件正常工作之外,还需要保证各个组件之间的调用正常。如果将每个组件视为一个微服务的话,每个微服务上暴露出的接口不止一个,组件两两之间的连接关系又不止一条。如此算来,在微服务规模不断增长的情况下,系统的复杂度已不止线性增长,因此整个系统的可靠性将会比各组件可靠性的乘积还要更低。

因此,在构建由微服务组成的复杂软件系统时,除了保证每个微服务组件的正确性之外,还必须通过测试验证微服务之间连接的正确性。

Pact 框架解决的就是如何测试微服务之间连接正确性的问题。

传统方式下,一般通过集成测试来验证服务之间调用是否正常,这需要将被测的各个组件均部署到环境中,然后再展开测试,且不说这种方式下测试运行得如何之慢,只是把各个服务都部署到同一环境之中这件工作,对于许多经常搭环境的开发者来说,就已经非常繁琐而痛苦了。而且由于多个服务共享同一环境,测试也比较脆弱,很容易挂。

Pact 是一个思路精巧、设计优雅的测试框架。它通过将一个笨重的集成测试化为两个可以独立运行的单元测试和接口测试来解决这一问题。两步即可完成:

  • Step 1: 服务消费者端编写单元测试,测试对服务提供者接口的客户端请求类。一运行测试,Pact 框架便帮助自动生成 json 形式的 pact 文件(契约文件)。pact 文件中含有交互的路径、方法、请求参数、请求头与期望响应等信息。
  • Step 2: 单独启动服务提供者(此时并不需要启动服务消费者),利用 Pact 框架提供的验证命令进行契约验证,基于上一步中生成的 pact 文件,对服务提供者的接口发送请求并验证实际响应是否与期望响应相符。

这种方式至少有明显的几个好处:

  • 服务测试解耦。两个测试之间唯一的耦合点是 pact 文件(自动生成),自然实现了服务消费者端与提供者端的测试解耦,再也不需要等待 “联调” 就能各自展开开发。
  • 消费者驱动。由于契约文件是由服务消费者的单元测试驱动而成的,服务提供者只要能够正确通过契约验证,就能保证满足服务消费者端的需求。这一思想其实与敏捷实践中的测试驱动开发(TDD)有异曲同工之处。
  • 测试前移。越早发现问题,解决问题的成本就越小。由于 Pact 测试已经足够轻量化,开发者甚至在本地开发阶段就可以进行相关的测试,而不需要等到集成阶段才暴露问题。

Pact的前世今生

Pact 是一个开源框架,最早是由澳洲最大的房地产信息提供商 REA Group 的开发者及咨询师们共同创造。REA Group 的开发团队很早便在项目中使用了微服务架构,并在团队中对于敏捷和测试的重要性早已形成共识,因此设计出这样的优秀框架并应用于日常工作中也是十分自然。

Pact 工具于 2013 年开始开源,发展到今天已然形成了一个小的生态圈,包括各种语言(Ruby/Java/.NET/JavaScript/Go/Scala/Groovy…)下的 Pact 实现,契约文件共享工具 Pact Broker 等。Pact 的用户已经遍及包括 RedHat、IBM、Accenture 等在内的若干知名公司,Pact 已经是事实上的契约测试方面的业界标准。

案例

所以,Pact 怎么工作的?它开始于消费者。

消费者服务的开发写一个测试。测试定义了与提供方的集成。这包括了提供方需要的状态,请求的消息体和期望的结果。基于这个定义 Pact 建立和运行一个提供方的桩来进行测试。这个测试的输出回事一个或多个 json 文件,一般是这样的:

{ "consumer": { "name": "billy" }, "provider": { "name": "bobby" }, "interactions": [ { "description": "My test", "providerState": "User billy exists", "request": { "method": "POST", "path": "/users/login", "headers": { "Content-Type": "application/json", }, "body": { "username":"billy", "password":"issilly" } }, "response": { "status": 200, } }, ], "metadata": { "pactSpecification": { "version": "2.0.0" } } }

这就是合约,这就是 pact。 现在他们被传给服务提供方。也可以被提交给共享的 git 仓库,或通过 Pact Broker 应用上传到共享的文件存储。

一旦合约更新过了 - 提供方需要对其进行测试验证是否仍符合要求。它通过使用共享的 pact 文件运行它自己的校验测试而不是真实版本的服务。如果所有的交互是符合预期的并测试通过了 - 我们就可以继续了。 如果不 - 提供方的开发需要通知消费方的开发。然后,他们可以一起分析什么导致了合约的失败。