使用Spring Cloud合约进行消费者驱动的合同测试
网址:https://specto.io/blog/2016/11/16/spring-cloud-contract/
汤米·斯德尔 2016年11月16日
随着系统拓扑的增长,测试微服务成为一项艰巨的任务。当微服务器链接在一起以实现业务功能时,通过编写集成测试来验证他们正在一起工作是很有挑战性的。如果您沿着这条路径走下去,您将需要拥有所有的应用程序,基础资源(如数据库,S3存储区)和第三方API在已知状态下连接并运行,以确保“服务A”可以通话到“服务B”。
事实上这是麻烦的设置不是唯一的问题在这里。你的测试有时可能会神秘地失败。“有时”可能意味着各种薄片,如网络超时,第三方API速率限制,或仅仅是从以前的测试运行留下的数据。如果您只想测试一个微服务器的API,则管理所有这些移动部件太多了。
幸运的是,可以使用像Hoverfly或WireMock这样的服务虚拟化工具来嘲笑依赖的服务。测试服务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模式。
- 运行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请求。为了使其可运行,我们还实现了引导测试环境的基类,如有必要,嘲笑依赖关系。
- 在生产者方面,我们实施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‘
}
- 现在我们可以实现生产者的新端点来通过测试。
@RequestMapping(method = RequestMethod.GET, value = "/account/{id}")
public Account getAccount(@PathVariable String id) {
return accountService.getById(id);
}
- 通过合同验证者考试后,我们有一个令人满意的合同!运行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框架是一个更好的选择。
- 作为一个新兴项目,您将期待看到一些出现问题。