使用Spring Cloud合约进行消费者驱动的合同测试

使用Spring Cloud合约进行消费者驱动的合同测试

网址:https://specto.io/blog/2016/11/16/spring-cloud-contract/

汤米·斯德尔 2016年11月16日

随着系统拓扑的增长,测试微服务成为一项艰巨的任务。当微服务器链接在一起以实现业务功能时,通过编写集成测试来验证他们正在一起工作是很有挑战性的。如果您沿着这条路径走下去,您将需要拥有所有的应用程序,基础资源(如数据库,S3存储区)和第三方API在已知状态下连接并运行,以确保“服务A”可以通话到“服务B”。

事实上这是麻烦的设置不是唯一的问题在这里。你的测试有时可能会神秘地失败。“有时”可能意味着各种薄片,如网络超时,第三方API速率限制,或仅仅是从以前的测试运行留下的数据。如果您只想测试一个微服务器的API,则管理所有这些移动部件太多了。

幸运的是,可以使用像HoverflyWireMock这样的服务虚拟化工具来嘲笑依赖的服务。测试服务A和B之间的集成成为服务A的隔离组件测试,其中嵌入了一个服务B。

然而,这又造成了另一个困境:您如何保证服务B的存根始终跟踪实际服务的更改?想象一下,在服务B工作的开发人员悄悄地推出一个API更新,使服务A使用的存根无效,并且连续的部署管道为基于服务A通过测试的发布提供了绿灯。这最终会导致生产中的消防。

也许现在是考虑两个服务之间的协议的时候了。服务A(作为消费者)创建一个服务B(作为制作人)必须遵守的合同。这种合同作为服务之间的隐形粘合剂 - 尽管它们分别独立于代码库并运行在不同的JVM上。在构建时可以立即检测到变化。

这被称为消费者驱动合同(CDC)测试,这是在分布式架构中测试服务虚拟化的有效方式。在本博客中,我将介绍Spring Cloud Contract:基于JVM的项目的CDC框架,特别是使用Spring Boot的项目

一个简单的用例

在这个演示中,我们有两个微服务器:订阅和帐户。我们需要为订阅服务添加新功能,以便对朋友的帐户进行订阅是免费的。要查明帐户是否标记为“朋友”,订阅服务需要使用帐户服务的“按ID获取帐户”API。您可以在GitHub找到此博客的源代码。

你需要什么

  • Java的
  • 弹簧启动(1.4.1.RELEASE)
  • Spring Cloud合约(1.0.1.RELEASE)
  • 毕业(3.1)
  • Maven仓库

Spring Cloud Contract项目网站上可以找到一个示例Gradle构建文件。

关键依赖关系是spring-cloud-starter-contract-verifier 生产者自动生成API验证测试,spring-cloud-starter-stub-runner 消费者自动配置存根服务器。

分步工作流程

CDC测试类似于架构/ API级别的TDD,因此共享类似的工作流程。

添加测试:  在消费者方面,我们首先编写新功能的功能测试,并实现与生产者端点通信的网关。

@RunWith(SpringRunner.class)
@SpringBootTest
public class SubscriptionTest {
   @Autowired
   private SubscriptionService service;

   @Test
   public void shouldGiveFreeSubscriptionForFriends() throws Exception {

       // given:
       String accountId = "12345";
       Subscription subscription = new Subscription(accountId, MONTHLY);

       // when:
       Invoice invoice = service.createInvoice(subscription);

       // then:
       assertThat(invoice.getPaymentDue()).isEqualTo(0);
       assertThat(invoice.getClientEmail()).isNotEmpty();
   }
}

运行所有测试:  显然它们失败了

org.springframework.web.client.ResourceAccessException: I/O error on GET request for "http://localhost:8082/account/12345": Connection refused.

编写一些代码:缺少的实现不再在同一个代码库中。我们需要查看生产者的存储库,并根据消费者期望生产者的行为方式,使用Spring Cloud Contract Groovy DSL添加合同。该文件应位于src/test/resources/contracts/ 的spring-cloud-contract-gradle-plugin发现。

package contracts
import org.springframework.cloud.contract.spec.Contract

Contract.make {
   request {
       method ‘GET‘
       url value(consumer(regex(‘/account/[0-9]{5}‘)), producer(‘/account/12345‘))
   }
   response {
       status 200
        body([
                type: ‘friends‘,
                email: ‘[email protected]‘
        ])
       headers {
           header(‘Content-Type‘: value(
                   producer(regex(‘application/json.*‘)),
                   consumer(‘application/json‘)
           ))
       }
   }
}

合同包括请求和响应对。它显示了使用URL路径的动态值的示例。使用值(consumer(...),producer(...))辅助方法,可以设置匹配器或具体值。在这种情况下,在消费者端(生成的存根)中添加正则表达式,以便将请求与任何帐户ID进行匹配,并为生成的测试设置特定的帐户ID,使其与生产者的已知状态相匹配。

再次,生产者方面遵循某种TDD模式。

  1. 运行gradle generateContractTests在生成文件夹中生成测试:
public class ContractVerifierTest extends ContractVerifierBase {

  @Test
  public void validate_shouldReturnFriendsAccount() throws Exception {
     // given:
        MockMvcRequestSpecification request = given();

     // when:
        ResponseOptions response = given().spec(request)
              .get("/account/12345");

     // then:
        assertThat(response.statusCode()).isEqualTo(200);
        assertThat(response.header("Content-Type")).matches("application/json.*");
     // and:
        DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
        assertThatJson(parsedJson).field("type").isEqualTo("friends");
        assertThatJson(parsedJson).field("email").isEqualTo("[[email protected]](/cdn-cgi/l/email-protection)<script data-cfhash="f9e31" type="text/javascript">/* <![CDATA[ */!function(t,e,r,n,c,a,p){try{t=document.currentScript||function(){for(t=document.getElementsByTagName(‘script‘),e=t.length;e--;)if(t[e].getAttribute(‘data-cfhash‘))return t[e]}();if(t&&(c=t.previousSibling)){p=t.parentNode;if(a=c.getAttribute(‘data-cfemail‘)){for(e=‘‘,r=‘0x‘+a.substr(0,2)|0,n=2;a.length-n;n+=2)e+=‘%‘+(‘0‘+(‘0x‘+a.substr(n,2)^r).toString(16)).slice(-2);p.replaceChild(document.createTextNode(decodeURIComponent(e)),c)}p.removeChild(t)}}catch(u){}}()/* ]]> */</script>");
  }
}

生成的测试依赖于RestAssuredMockMvc来执行HTTP请求。为了使其可运行,我们还实现了引导测试环境的基类,如有必要,嘲笑依赖关系。

  1. 在生产者方面,我们实施ContractVerifierBase类来加载Web上下文并设置RestAssuredMockMvc
@Ignore
@RunWith(SpringRunner.class)
@SpringBootTest(classes = AccountServiceApplication.class)
public class ContractVerifierBase {

   @Autowired
   private WebApplicationContext context;

   @Before
   public void setUp() throws Exception {
       RestAssuredMockMvc.webAppContextSetup(context);
   }
}

我们还需要在build.gradle文件中进行以下设置来告诉spring-cloud-contract插件找到ContractVerifierBase类:

contracts {
    packageWithBaseClasses = ‘com.demo.account.contracts‘
}
  1. 现在我们可以实现生产者的新端点来通过测试。
@RequestMapping(method = RequestMethod.GET, value = "/account/{id}")
public Account getAccount(@PathVariable String id) {
   return accountService.getById(id);
}
  1. 通过合同验证者考试后,我们有一个令人满意的合同!运行gradle clean build install将生成并发布WireMock映射作为stubs.jar文件到本地的maven仓库。您可以检查文件build/mappings夹中的WireMock映射文件:
{
 "uuid" : "79ab1fad-984f-4a6c-8b24-88deeb8cb503",
 "request" : {
   "urlPattern" : "/account/[0-9]{5}",
   "method" : "GET"
 },
 "response" : {
   "status" : 200,
   "body" : "{\"type\":\"friends\",\"email\":\"[[email protected]](/cdn-cgi/l/email-protection)<script data-cfhash="f9e31" type="text/javascript">/* <![CDATA[ */!function(t,e,r,n,c,a,p){try{t=document.currentScript||function(){for(t=document.getElementsByTagName(‘script‘),e=t.length;e--;)if(t[e].getAttribute(‘data-cfhash‘))return t[e]}();if(t&&(c=t.previousSibling)){p=t.parentNode;if(a=c.getAttribute(‘data-cfemail‘)){for(e=‘‘,r=‘0x‘+a.substr(0,2)|0,n=2;a.length-n;n+=2)e+=‘%‘+(‘0‘+(‘0x‘+a.substr(n,2)^r).toString(16)).slice(-2);p.replaceChild(document.createTextNode(decodeURIComponent(e)),c)}p.removeChild(t)}}catch(u){}}()/* ]]> */</script>\"}",
   "headers" : {
     "Content-Type" : "application/json"
   }
 }
}

再次运行测试:最后,在消费者端,我们只是添加

@AutoConfigureStubRunner(ids = "com.demo:account-service:+:stubs:8082", workOffline = true)

到需要生产者存根的测试。这个存根运行程序将拉取并解压缩最新的存根jar文件(当我们将版本设置为“+”符号)时,在端口8082上启动WireMock服务器并注册存根映射。

现在我们有生产者存根运行,测试应该通过。

在CI / CD环境中工作

到目前为止,我们只看到如何在本地机器上开发CDC的新功能。与包/构建管道集成需要更多的调整:

  • 默认情况下,生产者的Gradle构建任务将生成并运行合同验证程序测试。它只需要通过添加uploadArchives到其Gradle任务将存根jar发布到远程存储库。
  • 该消费者需要配置StubRunner解决存根。这可以通过设置Spring Boot应用程序属性来实现:
stubrunner:
    ids: com.demo:account-service:+:stubs:8082
    repositoryRoot: https://demo.jfrog.io/demo/libs-snapshot</pre>

结论

消费者驱动的合同(CDC)为我们提供了快速的反馈,以验证微服务之间的集成,以及在独立部署时有更多的信心,而不用担心对其他服务引入突破性的更改。

Spring Cloud合同为CDC测试提供了一个简单的工作流程,并以最小的编码。优点是您可以使用静态类型的Groovy DSL编写合同,以自动生成生成器验证测试和存根映射文件。缺点是手工制作合同文件在某些??情况下可能是诅咒。例如,服务交互可能具有复杂的有效载荷或请求主体,并且需要花费大量的精力才能使其正确。

还有一些注意事项:

  • 您的CI环境应该与maven存储库集成,以共享存根jar文件。Spring Cloud Contract在写作时尚未支持从密码保护的存储库中解析存根。
  • 仅支持基于JVM的项目。如果您正在为Javascript,Go,.Net等寻找CDC框架,  Pact框架是一个更好的选择。
  • 作为一个新兴项目,您将期待看到一些出现问题

如果您对此演示的源代码感兴趣,可以在GitHub找到

时间: 2024-10-21 10:36:36

使用Spring Cloud合约进行消费者驱动的合同测试的相关文章

Spring Cloud Contract 微服务契约测试框架

简介 使用场景 主要用于在微服务架构下做CDC(消费者驱动契约)测试.下图展示了多个微服务的调用,如果我们更改了一个模块要如何进行测试呢? 传统的两种测试思路 模拟生产环境部署所有的微服务,然后进行测试 优点 测试结果可信度高 缺点 测试成本太大,装一整套环境耗时,耗力,耗机器 Mock其他微服务做端到端的测试 优点 不用装整套产品了,测的也方便快捷 缺点 需要写很多服务的Mock,要维护一大堆不同版本用途的simulate(模拟器),同样耗时耗力 Spring Cloud Contrct解决思

Spring Cloud Stream教程(四)消费群体

虽然发布订阅模型可以轻松地通过共享主题连接应用程序,但通过创建给定应用程序的多个实例来扩展的能力同样重要.当这样做时,应用程序的不同实例被放置在竞争的消费者关系中,其中只有一个实例预期处理给定消息. Spring Cloud Stream通过消费者组的概念来模拟此行为.(Spring Cloud Stream消费者组与Kafka消费者组相似并受到启发.)每个消费者绑定可以使用spring.cloud.stream.bindings..group属性来指定组名称.对于下图所示的消费者,此属性将设置

Spring cloud stream【消息分组】

??上篇文章我们简单的介绍了stream的使用,发现使用还是蛮方便的,但是在上个案例中,如果有多个消息接收者,那么消息生产者发送的消息会被多个消费者都接收到,这种情况在某些实际场景下是有很大问题的,比如在如下场景中,订单系统我们做集群部署,都会从RabbitMQ中获取订单信息,那如果一个订单同时被两个服务获取到,那么就会造成数据错误,我们得避免这种情况.这时我们就可以使用Stream中的消息分组来解决了! Stream消息分组 ??消息分组的作用我们已经介绍了.注意在Stream中处于同一个gr

spring cloud延时队列的使用

用户购买一笔订单,需要在订单的有效截止时间前一定时间,提醒用户去使用.到达有效结束时间,将订单设置为失效.这时候可以用延时队列可以很好的解决,用户下单之后,在有效期前发送一条提醒用户去使用的消息,和一条订单已经失效的消息. 入口 /** * 爆品助力状态提醒 * * @param req 爆品助力失败 */ @RequestMapping(path = "/mq/product/sendProductHelpStatusMessage", method = RequestMethod.

Spring Cloud 是什么

概念定义 Spring Cloud 是一个服务治理平台,提供了一些服务框架.包含了:服务注册与发现.配置中心.消息中心 .负载均衡.数据监控等等. Spring Cloud 是一个微服务框架,相比 Dubbo 等 RPC 框架,Spring Cloud 提供了全套的分布式系统解决方案. Spring Cloud 对微服务基础框架 Netflix 的多个开源组件进行了封装,同时又实现了和云端平台以及 Spring Boot 框架的集成. Spring Cloud 是一个基于 Spring Boot

朱晔和你聊Spring系列S1E11:小测Spring Cloud Kubernetes @ 阿里云K8S

朱晔和你聊Spring系列S1E11:小测Spring Cloud Kubernetes @ 阿里云K8S 有关Spring Cloud Kubernates(以下简称SCK)详见https://github.com/spring-cloud/spring-cloud-kubernetes,在本文中我们主要测试三个功能: 使用Kubernetes服务发现配合Spring Cloud Ribbon做服务调用 读取Kubernetes的ConfigMap配置并且支持修改后动态刷新 Spring Bo

(十七)JAVA springcloud ssm b2b2c多用户商城系统-消息驱动 Spring Cloud Stream

在使用spring cloud云架构的时候,我们不得不使用Spring cloud Stream,因为消息中间件的使用在项目中无处不在,我们公司后面做了娱乐方面的APP,在使用spring cloud做架构的时候,其中消息的异步通知,业务的异步处理都需要使用消息中间件机制.spring cloud的官方给出的集成建议(使用rabbit mq和kafka),我看了一下源码和配置,只要把rabbit mq集成,kafka只是换了一个pom配置jar包而已,闲话少说,我们就直接进入配置实施: 1. 简

第十章 消息驱动的微服务: Spring Cloud Stream

Spring Cloud Stream 是一个用来为微服务应用构建消息驱动能力的框架. 它可以基于Spring Boot 来创建独立的. 可用于生产的 Spring 应用程序. 它通过使用 Spring Integration 来连接消息代理中间件以实现消息事件驱动. Spring Cloud Stream 为一些供应商的消息中间件产品提供了个性化的自动化配置实现,并且引入了发布-订阅. 消费组以及分区这三个核心概念. 简单地说, Spring Cloud Stream 本质上就是整合了 Spr

Spring Cloud构建微服务架构 消息驱动的微服务(消费分区)【Dalston版】

通过上一篇<消息驱动的微服务(消费组)>的学习,我们已经能够在多实例环境下,保证同一消息只被一个消费者实例进行接收和处理.但是,对于一些特殊场景,除了要保证单一实例消费之外,还希望那些具备相同特征的消息都能够被同一个实例进行消费.这时候我们就需要对消息进行分区处理. 使用消息分区 在Spring Cloud Stream中实现消息分区非常简单,我们可以根据消费组示例做一些配置修改就能实现,具体如下: 在消费者应用SinkReceiver中,我们对配置文件做一些修改,具体如下: spring.c