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

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用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找到

以上是关于使用Spring Cloud合约进行消费者驱动的合同测试的主要内容,如果未能解决你的问题,请参考以下文章

Spring Cloud 合约功能

在同一个项目中配置spring cloud合约和zuul代理

Spring Cloud 入门教程 - 搭建配置中心服务

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

Spring Cloud Stream 消息驱动 RabbitMQ 基础使用

Spring Cloud 系列之 Stream 消息驱动