上一篇博客介绍了如何用Pact-JS编写HTTP协议的接口的契约测试,实际,Pact-JS除了能对通过HTTP协议接口交互的服务编写契约测试外,还可以对通过发送Message进行交互的Provider和Consumer编写契约测试,还可以对通过GraphQL进行查询的服务编写契约测试。接下来先看看GraphQL的场景如何编写契约测试。
Pact-JS给GraphQL场景编写契约测试
还是从官网的例子出发来看看如何编写,再从实现原理上理解下背后实现过程。下图中左边是provider端的代码,代码启动了一个服务,监听在4000端口,服务暴露了一个API url (/graphql),通过这个url可以查询graphql获取相关的数据。下图右边是consumer端的代码,用ApolloClient调用graphql暴露在4000端口上的服务。可以看到graphql和HTTP协议的接口请求非常类似,只是在调用查询语句上略有不同而已,本质上都是基于HTTP协议的。
再来看看consumer端的测试,再new Pact(...)中,定义了port端口,consumer/provider名称等信息,这里的端口实际就是Pact启动mock服务的端口,这里mock的端口和真实服务是一样的端口,如果不一致,那么在consumer端,要通过环境变量提取graphql的baseUrl做替换,最终才能用mock服务替换真实的服务。中间截图部分是mock graphql的request和response,截图右边部分是consumer的真正测试部分,这里调用consumer端的query()function,验证该function的结果是否符合预期。
provider端的测试代码和HPTT协议接口一致,这里不再介绍,可以看到,因为graphql的调用和基于http协议的接口调用很相似,所以,在写契约测试时,思路也是相同的,只是mock request的细节上有稍微区别,背后的实现原理也相同,即在consumer端运行测试时,实际是启动了一个mock服务带代替第三方,实现consumer端的测试。
用Pact-JS给异步交互(Message)场景编写契约测试
除了给GraphQL调用编写契约测试外,还可以给异常场景,即通过发送消息到queue进行交互的服务编写契约测试。同样的步骤,先从官网给的样例代码开始。下面的代码左下图是provider端的代码,有一个createDog的方法来模拟创建message,message的内容如下图右边所示,中间是consumer端代码,代码中有个dogApiHandler()的方法,模拟接受、处理message(实际就是Dog对象)。可以看到官网的example代码中provider和consumer端的代码写的非常简单。
为了后面更好的理解Pact如何实现message场景的契约测试,我们先来看一下如何要基于Queue编写provider和consumer,大致的代码应该是如何的。下面代码是使用RabbitMQ作为消息队列中间件来模拟整个流程,使用amqplib库来连接 RabbitMQ,并分别编写 Provider 和 Consumer 的代码。Provider 在连接 RabbitMQ 后将一条消息发送到名为 "message_queue" 的队列中,并打印发送的消息。Consumer 也连接 RabbitMQ,并从队列 "message_queue" 中消费消息,并在控制台上打印接收到的消息。从下面的代码可以看到,在发送或者接受消息时,需要连接队列服务地址,例如RabbitMQ服务的地址,这里假设是:amqp://localhost,连接后,发送和接受消息时通过queueName进行match的,即queueName确定了从那条queue里面发送或者接受消息。
对比Pact-JS官网给的例子,写的比较粗糙,并没有编写较为完善的provider和consumer端代码。接着继续看consumer端和provider端的契约测试。consumer部分的契约测试,前面部分还是对期望的message做了一些模拟,然后验证的是:.verify(aynchronousBodyHandler(dogApiHandler)),这样就完成了consumer端的契约测试。对于Provider端,在new MessageProviderPact部分调用了createDog()方法,然后调用p.verify()进行了验证。从契约测试的代码来看,有点难以理解是如何完成验证的。
从理论上讲,因为发送和接受消息也是需要RabbitMQ等的服务地址,所以,如果要实现consumer端的契约测试,显然,Pact需要mock的mq出来,这样可以往mock的mq中发送消息,用mock的mq消息的服务地址替换真实的地址,这样就可以完成Consumer端的契约测试,但从上面的契约测试脚本来看,Pact显然不是这个实现原理,那么Pact是如何实现message的契约测试的呢?来看看官网的一段解释:
Pact 将发送消息给您的消息处理程序。如果处理程序返回成功的 Promise,则消息将被保存;否则,测试将失败。需要考虑几个关键点:
Pact 实际发送的请求主体将包含在 Message 对象中,同时包含其他上下文信息,因此必须通过 content 属性获取主体内容。要测试的所有处理程序都必须具有 (m: Message) => Promise<any> 的形式 - 也就是说,它们必须接受一个 Message 并返回一个 Promise。这是我们绕过所有不同协议的方法,并且通常需要一个轻量级的适配器函数来进行转换。在这种情况下,我们使用 Pact 提供的 synchronousBodyHandler 便利函数来包装实际的 dogApiHandler,它将处理程序转换成 Promise,并提取其内容。从官网的解释来看,如果consumer端要实现契约测试,那么consumer端的所有消息处理handler必须符合上面的规则,这个handler方法返回的必须是一个Promise。这样Pact才能将需要模拟的Message替换成Handler中传入的Message,再验证Handler返回的Promise内容。从这里看,Pact编写基于Message的服务的契约测试是由前置条件的。
再来看看Provider部分,对于基于HTTP协议的接口,provider端是启动真实的服务,发送请求,查看真实的请求和response与契约中定义是否一致来完成provider端的契约测试。但是,在消息契约测试中,似乎并不是这样。对于消息,如何要进行验证,应该是在消息发送过程中,拦截下消息内容,与契约中期望的消息进行对比。在上面的例子中,只是调用createDog的方法,这个方法返回的是一个Promise,理论上Pact需要实现提取出消息的content部分出来,完成与契约文件的对比。实际是否这样呢?来看看官网的说明:
我们的 API 生产者包含一个名为 createDog 的函数,负责生成将通过某个消息队列发送给消费者的消息。我们配置 Pact 作为消息队列的替代品。这里最重要的部分是 messageProviders 块。
与 Consumer 测试类似,我们通过其 description 字段将要验证的各种交互映射到相应的处理程序(handler)。在这种情况下,请求狗的操作映射到 createDog 处理程序。请注意,这与原始的 Consumer 测试匹配。我们使用 providerWithMetadata 函数,因为我们还要验证消息的元数据(在本例中是消息将要发送到的队列)。现在,我们可以运行验证过程。Pact 将读取其 Consumer 指定的所有交互,并调用负责生成该消息的每个函数。从描述来看,provider端负责生成消息的Handler()也必须符合规则才行。总结来说,用Pact给发送message的场景编写契约测试是与前置条件的。