07 契约测试:如何进行消费者驱动的契约测试? 上一课时,我讲到了微服务架构下的组件测试,它是针对单个微服务的验收测试,虽然保障了单个微服务功能的正确性,但要想保障微服务间交互功能的正确性,就需要进行契约测试。

契约测试产生的背景

在介绍契约测试之前,首先来看下什么是契约。现实世界中,契约是一种书面的约定,比如租房时需要跟房东签房屋租赁合同、买房时需要签署购房合同、换工作时你要跟公司签署劳动合同等。在信息世界中,契约也有很多使用场景,像 TCP/IP 协议簇、HTTP 协议等,只是这些协议已经成为一种技术标准,我们只需要按标准方式接入就可以实现特定的功能。

具体到业务场景中,契约是研发人员在技术设计时达成的约定,它规定了服务提供者和服务消费者的交互内容。可见,无论是物理世界还是信息世界,契约是双方或多方共识的一种约定,需要协同方共同遵守。

在微服务架构中,服务与服务之间的交互内容更需要约定好。因为一个微服务可能与其他 N 个微服务进行交互,只有对交互内容达成共识并保持功能实现上的协同,才能实现业务功能。我们来看一个极简场景,比如我们要测试服务 A 的功能,然而需要服务 A 调用服务 B 才能完成,如图:

Drawing 0.png

服务 A 所属的研发测试团队在测试时,太难保证服务 B 是足够稳定的,而服务 B 的不稳定会导致测试服务 A 时效率下降、测试稳定性降低。因为,当服务 B 有阻塞性的缺陷或者干脆宕机时,你需要判断是环境问题还是功能缺陷导致的,这些情况在微服务的测试过程中属于常见的痛点问题。因此,为了提升测试效率和测试稳定性,我们会通过服务虚拟化技术来模拟外部服务,如图:

Drawing 2.png

需要特别注意的是,如果此时你针对内部系统的测试用例都执行通过了,可以说明你针对服务 A的测试是通过的吗?答案是否定的。因为这里面有个特别重要的假设是,服务虚拟化出来的Mock B 服务与真实的 B 服务是相等的。而事实是,它们可能只在你最初进行服务虚拟化时是相等的,随着时间的推移,它们很难保持相等。

Drawing 4.png

可能你会说,保持相等不就是个信息同步的工作嘛,有那么难吗?事实上,说起来容易做起来真的挺难:在实际的研发场景下,一个研发团队需要维护若干(a)个服务,每个服务又有数十(b)个接口,每个接口又被多(c)个团队的服务所调用,可见信息同步的工作量是巨大的(a/b/c)。

所以在微服务团队中,如下情况极为常见,每一项都会导致信息不同步:服务 B 的开发团队认为某次修改对服务 A 无影响,所以没告诉服务 A 的开发团队,而实际上是有影响的;服务 B 的开发团队认为某次修改对服务 A 有影响,而服务 A 的开发团队认为无影响;服务 B 的开发团队忘记把某次修改同步到服务 A 的开发团队。

所以,比较好的方式就是通过“契约”来降低服务 A 和服务 B 的依赖。具体指导原则为:

  • 根据服务 A 和服务 B 的交互生成一份“契约”,且契约内容的变化可以及时感知到,并生成模拟服务;
  • 将服务之间的集成测试,变成两个测试,即真实的服务 A 和模拟服务 B 之间的测试和模拟的服务 A 和真实服务 B 之间的测试。

Drawing 6.png

契约测试示意图

理解了契约测试产生的背景,我们来讲解下微服务架构下契约测试的具体含义。

契约测试介绍

在微服务架构下,契约(Contract)是指服务的消费者(Consumer)与服务的提供者(Provider)之间交互协作的约定。契约主要包括两部分。

  • 请求(Request):指消费者发出的请求,通常包括请求头(Header)、请求内容(URI、Path、HTTP Verb)、请求参数及取值类型和范围等。
  • 响应(Response):指提供者返回的响应。可能包括响应的状态码(Status Word)、响应体的内容(XML/JSON) 或者错误的信息描述等。

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

为什么要进行消费者驱动的契约测试呢?在微服务架构下,提供者和消费者往往是一对多的关系。比如,服务提供者提供了一个 API,该服务会被多个不同的消费者所调用,当提供者想要修改该 API 时,就需要知道该 API 当前正在被多少消费者所调用,具体是怎样调用的。否则,当提供者针对该 API 进行逻辑或字段的修改(新增、删除、更新)时,都有可能导致消费者无法正常使用。而消费者驱动的契约测试相当于把不同消费者对该 API 的需求暴露出来,形成契约文件和验证点,提供者完成功能修改后对修改结果进行验证,以保障符合消费者的预期。

工欲善其事,必先利其器。要想做某类测试,一个好的测试框架是必不可少的。在契约测试领域也有不少测试框架,其中两个比较成熟的企业级测试框架:

  • Spring Cloud Contract,它是 Spring 应用程序的消费者契约测试框架;
  • Pact 系列框架,它是支持多种语言的框架。

因为 Pact 的多语言特性,它也是实际工作过程中使用最频繁的框架。为了加深对契约测试的理解,我们来看一个基于 Pact 框架的契约测试的实例。

契约测试实例

契约内容

如下所示,服务提供者为 userservice,消费者为 ui,契约内容包含了 POST 请求 /user-service/users,传参为对象 user, 并返回 201 和创建用户的 id。 { “consumer”: { “name”: “ui” }, “provider”: { “name”: “userservice” }, “interactions”: [ { “description”: “a request to POST a person”, “providerState”: “provider accepts a new person”, “request”: { “method”: “POST”, “path”: “/user-service/users”, “headers”: { “Content-Type”: “application/json” }, “body”: { “firstName”: “Arthur”, “lastName”: “Dent” } }, “response”: { “status”: 201, “headers”: { “Content-Type”: “application/json” }, “body”: { “id”: 42 }, “matchingRules”: { “$.body”: { “match”: “type” } } } } ], “metadata”: { “pactSpecification”: { “version”: “2.0.0” } } }

Spring Controller

创建 Spring Controller,并遵循上述的契约; @RestController public class UserController { private UserRepository userRepository; @Autowired public UserController(UserRepository userRepository) { this.userRepository = userRepository; } @PostMapping(path = “/user-service/users”) public ResponseEntity createUser(@RequestBody @Valid User user) { User savedUser = this.userRepository.save(user); return ResponseEntity .status(201) .body(new IdObject(savedUser.getId())); } }

服务提供者测试

为了快速发现问题,最好在每次构建时都进行契约测试,可以使用 Junit 来管理测试。

要创建 Junit 测试,需要添加依赖到工程中: dependencies { testCompile(“au.com.dius:pact-jvm-provider-junit5_2.12:3.5.20”) // Spring Boot dependencies omitted }

创建服务提供者测试 UserControllerProviderTest,并运行:

@ExtendWith(SpringExtension.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT, properties = “server.port=8080”) @Provider(“userservice”) @PactFolder(“../pact-angular/pacts”) public class UserControllerProviderTest { @MockBean private UserRepository userRepository; @BeforeEach void setupTestTarget(PactVerificationContext context) { context.setTarget(new HttpTestTarget(“localhost”, 8080, “/”)); } @TestTemplate @ExtendWith(PactVerificationInvocationContextProvider.class) void pactVerificationTestTemplate(PactVerificationContext context) { context.verifyInteraction(); } @State({“provider accepts a new person”}) public void toCreatePersonState() { User user = new User(); user.setId(42L); user.setFirstName(“Arthur”); user.setLastName(“Dent”); when(userRepository.findById(eq(42L))).thenReturn(Optional.of(user)); when(userRepository.save(any(User.class))).thenReturn(user); } }

测试结果如下所示:

Verifying a pact between ui and userservice Given provider accepts a new person a request to POST a person returns a response which has status code 201 (OK) includes headers “Content-Type” with value “application/json” (OK) has a matching body (OK)

也可以将契约文件上传到 PactBroker 中,这样后续测试时可以直接从 PactBroker 中加载契约文件:

@PactBroker(host = “host”, port = “80”, protocol = “https”, authentication = @PactBrokerAuth(username = “username”, password = “password”)) public class UserControllerProviderTest { … }

总结

本节课我首先讲解了契约的定义,通俗地讲,它是双方或多方共识的一种约定,需要协同方共同遵守。而在微服务架构下,契约(Contract)是指服务的消费者(Consumer)与服务的提供者(Provider)之间交互协作的约定,主要包括请求和响应两部分。

紧接着讲解了微服务架构下跨服务测试的痛点和难点,因而引入了契约测试的概念,它的指导思想是通过“契约”来降低服务和服务之间的依赖,即,将契约作为中间标准,对消费者与提供者间的协作进行的验证。根据测试对象的不同,契约测试分为两种,但最常用的契约测试类型是消费者驱动的契约测试(Consumer-Driven Contract Test,简称 CDC)。核心思想是从消费者业务实现的角度出发,由消费者端定义需要的数据格式以及交互细节,生成一份契约文件。然后提供者根据契约文件来实现自己的逻辑,并在持续集成环境中持续验证该实现结果是否正确。契约测试框架也有多种,但最常见的框架有 Spring Cloud Contract 和 Pact,其中 Pact 框架更为流行。

最后给出了基于 Pact 框架的契约测试实例的大体步骤,并在文稿下方给出了示例代码地址,感兴趣的同学可以自行学习。

你所负责的项目或服务,是否进行过契约测试呢?如果有,是哪种类型的契约测试,具体的进展是怎样的?欢迎在留言区评论。同时欢迎你能把这篇文章分享给你的同学、朋友和同事,大家一起交流。 相关链接 https://www.martinfowler.com/articles/microservice-testing/ https://reflectoring.io/7-reasons-for-consumer-driven-contracts/ 契约测试框架 https://docs.pact.io/ https://spring.io/projects/spring-cloud-contract https://www.infoq.com/news/2019/02/contract-testing-microservices/ 实例 https://github.com/thombergs/code-examples/tree/master/pact/pact-spring-provider https://reflectoring.io/consumer-driven-contract-provider-pact-spring/

参考资料

https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e5%be%ae%e6%9c%8d%e5%8a%a1%e8%b4%a8%e9%87%8f%e4%bf%9d%e9%9a%9c%2020%20%e8%ae%b2-%e5%ae%8c/07%20%20%e5%a5%91%e7%ba%a6%e6%b5%8b%e8%af%95%ef%bc%9a%e5%a6%82%e4%bd%95%e8%bf%9b%e8%a1%8c%e6%b6%88%e8%b4%b9%e8%80%85%e9%a9%b1%e5%8a%a8%e7%9a%84%e5%a5%91%e7%ba%a6%e6%b5%8b%e8%af%95%ef%bc%9f.md