learn-tech/专栏/微服务质量保障20讲-完/07契约测试:如何进行消费者驱动的契约测试?.md
2024-10-16 06:37:41 +08:00

12 KiB
Raw Blame History

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

契约测试产生的背景

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

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

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

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

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

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

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

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

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

契约测试示意图

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

契约测试介绍

在微服务架构下契约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/