消费者驱动的契约测试
相当早以前,我们从REST(ful) Web API的角度讨论了消费者驱动的合同测试 ,尤其是将其投射到Java( JAX-RS 2.0规范)的角度。 可以公平地说,至少在公共API方面, REST仍在Web API领域占据主导地位,但是向微服务或/和基于服务的体系结构的转变正在Swift改变力量的一致性。 这种破坏性趋势之一是消息传递 。
现代的REST(ful) API主要通过HTTP 1.1协议实现,并受其请求/响应通信样式的限制。 这里提供了HTTP / 2的 帮助,但并不是每个用例都适合此通信模型。 通常,该工作可以异步执行,并且可以稍后将其完成的事实广播给感兴趣的各方。 这就是大多数事物在现实生活中的工作方式,而使用消息传递是对此的完美答案。
消息传递空间确实挤满了惊人数量的消息代理和可用的无代理选项。 我们将不讨论这个问题,而是关注另一个棘手的主题:消息契约。 生产者发出消息或事件后,它将进入队列/主题/频道,准备被使用。 它在这里停留了一段时间。 显然,生产者知道它所发布的内容,但是消费者呢? 他们怎么知道会发生什么?
此刻,我们许多人会大喊:使用基于模式的序列化! 的确, Apache Avro , Apache Thrift , 协议缓冲区 , 消息包 …可以解决这个问题。 归根结底,此类消息和事件以及REST(ful) Web API(如果有)将成为提供者合同的一部分,并且必须随着时间的推移进行通信和发展,而又不会破坏消费者。 但是……您会惊讶地发现,有多少组织在JSON中发现了他们的必杀技,并使用它来传递消息和事件, 从而向消费者扔出这样的垃圾 ,而没有任何模式! 在这篇文章中,我们将研究消费者驱动的合同测试技术如何在这种情况下为我们提供帮助。
让我们考虑一个简单的系统,该系统具有两项服务,即订购服务和货运服务 。 订单服务将消息/事件发布到消息队列,然后运货服务从那里使用它们。
由于Order Service是用Java实现的,因此事件只是POJO类,在使用大量库之一到达消息代理之前,序列化为JSON 。 OrderConfirmed是此类事件之一。
public class OrderConfirmed {
private UUID orderId;
private UUID paymentId;
private BigDecimal amount;
private String street;
private String city;
private String state;
private String zip;
private String country; }
通常情况下, Shipment Service团队会交出示例JSON代码片段,或者指出一些文档片段或参考Java类,基本上就是这样。 在确保他们的解释正确无误且所需信息消息不会突然消失的同时,货运服务团队如何启动整合工作? 以消费者为导向的合同测试得以营救!
Shipment Service团队可以(并且应该)开始针对OrderConfirmed消息编写测试用例,并嵌入他们所拥有的知识,而我们的老朋友Pact框架(准确地说,是Pact JVM )是实现此目的的正确工具。 那么测试用例可能是什么样子?
public class OrderConfirmedConsumerTest {
private static final String PROVIDER_ID = "Order Service" ;
private static final String CONSUMER_ID = "Shipment Service" ;
@Rule
public MessagePactProviderRule provider = new MessagePactProviderRule( this );
private byte [] message;
@Pact (provider = PROVIDER_ID, consumer = CONSUMER_ID)
public MessagePact pact(MessagePactBuilder builder) {
return builder
.given( "default" )
.expectsToReceive( "an Order confirmation message" )
.withMetadata(Map.of( "Content-Type" , "application/json" ))
.withContent( new PactDslJsonBody()
.uuid( "orderId" )
.uuid( "paymentId" )
.decimalType( "amount" )
.stringType( "street" )
.stringType( "city" )
.stringType( "state" )
.stringType( "zip" )
.stringType( "country" ))
.toPact();
}
@Test
@PactVerification (PROVIDER_ID)
public void test() throws Exception {
Assert.assertNotNull(message);
}
public void setMessage( byte [] messageContents) {
message = messageContents;
} }
它非常简单明了,没有添加任何样板。 测试用例是根据OrderConfirmed消息的JSON表示而设计的。 但是我们还只是半途而废, 货运 服务团队应该以某种方式将他们的期望回馈给订购服务,以便生产者可以跟踪谁以及如何消费OrderConfirmed消息。 Pact测试工具通过将每个JUnit测试用例中的pact文件(一组协议或pact)生成到“ target / pacs”文件夹中来解决此问题。 下面是运行OrderConfirmedConsumerTest测试套件后生成的Shipment Service-Order Service.json pact文件的示例 。
{
"consumer" : {
"name" : "Shipment Service"
},
"provider" : {
"name" : "Order Service"
},
"messages" : [
{
"description" : "an Order confirmation message" ,
"metaData" : {
"contentType" : "application/json"
},
"contents" : {
"zip" : "string" ,
"country" : "string" ,
"amount" : 100 ,
"orderId" : "e2490de5-5bd3-43d5-b7c4-526e33f71304" ,
"city" : "string" ,
"paymentId" : "e2490de5-5bd3-43d5-b7c4-526e33f71304" ,
"street" : "string" ,
"state" : "string"
},
"providerStates" : [
{
"name" : "default"
}
],
"matchingRules" : {
"body" : {
"$.orderId" : {
"matchers" : [
{
"match" : "regex" ,
"regex" : "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
}
],
"combine" : "AND"
},
"$.paymentId" : {
"matchers" : [
{
"match" : "regex" ,
"regex" : "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
}
],
"combine" : "AND"
},
"$.amount" : {
"matchers" : [
{
"match" : "decimal"
}
],
"combine" : "AND"
},
"$.street" : {
"matchers" : [
{
"match" : "type"
}
],
"combine" : "AND"
},
"$.city" : {
"matchers" : [
{
"match" : "type"
}
],
"combine" : "AND"
},
"$.state" : {
"matchers" : [
{
"match" : "type"
}
],
"combine" : "AND"
},
"$.zip" : {
"matchers" : [
{
"match" : "type"
}
],
"combine" : "AND"
},
"$.country" : {
"matchers" : [
{
"match" : "type"
}
],
"combine" : "AND"
}
}
}
}
],
"metadata" : {
"pactSpecification" : {
"version" : "3.0.0"
},
"pact-jvm" : {
"version" : "4.0.2"
}
} }
发货服务团队的下一步是与订单服务团队共享此契约文件,以便这些人可以在其测试套件中运行提供方的契约验证。
@RunWith (PactRunner. class ) @Provider (OrderServicePactsTest.PROVIDER_ID) @PactFolder ( "pacts" @PactFolder "pacts" ) public class OrderServicePactsTest {
public static final String PROVIDER_ID = "Order Service" ;
@TestTarget
public final Target target = new AmqpTarget();
private ObjectMapper objectMapper;
@Before
public void setUp() {
objectMapper = new ObjectMapper();
}
@State ( "default" )
public void toDefaultState() {
}
@PactVerifyProvider ( "an Order confirmation message" )
public String verifyOrderConfirmed() throws JsonProcessingException {
final OrderConfirmed order = new OrderConfirmed();
order.setOrderId(UUID.randomUUID());
order.setPaymentId(UUID.randomUUID());
order.setAmount( new BigDecimal( "102.33" ));
order.setStreet( "1203 Westmisnter Blvrd" );
order.setCity( "Westminster" );
order.setCountry( "USA" );
order.setState( "MI" );
order.setZip( "92239" );
return objectMapper.writeValueAsString(order);
} }
测试工具从@PactFolder中选取所有的pact文件,并针对@TestTarget进行测试,在这种情况下,我们将接线提供的AmqpTarget ,但您可以轻松插入自己的特定目标。
基本上就是这样! 消费者( 装运服务 )在测试用例中表达了他们的期望,并以契约文件的形式与生产者( 订购服务 )共享了他们的期望。 生产者有自己的一组测试,以确保其模型符合消费者的观点。 双方可以继续独立发展,并相互信任,只要条约不受到谴责(希望永远不会)。
公平地说, Pact并不是进行消费者驱动的合同测试的唯一选择,在即将发布的帖子(已经开始工作)中,我们将讨论另一个出色的选择,即Spring Cloud Contract 。
直到今天,完整的项目资源都可以在Github上找到 。
消费者驱动的契约测试