契约测试是一种通过对每个应用程序进行孤立检查,以确保其发送或接收的消息符合在“合同”中记录的共享理解的集成点测试技术。对于通过HTTP进行通信的应用程序,这些“消息”将是HTTP请求和响应,而对于使用队列的应用程序,则是放入队列中的消息。在实践中,实施契约测试的常见方式是:检查所有对测试替身的调用是否返回与对真实应用程序的调用相同的结果。
契约测试尝用于微服务中,当一个系统由多个微服务组成时,服务于服务中间通过接口或者消息进行交互,服务间的沟通就变得至关重要,即provider端的任何修改都要能及时通知到consumer端,这样才不会导致consumer端的失败。契约测试就是有效解决provider端和consumer端沟通的一种测试手段,保证provider端提供的服务于consumer端使用的服务是一致的,不用等到服务集成测试时,才发现不一致。
实施契约测试的工具主要有两个,Pact和SCC(Spring-cloud-contract),SCC需要与Spring的框架配合使用,所以,相对来说,Pact的使用面积更广,Pact提供了Pact-JVM,Pact-JS,Pact-Go,Pact-Python等客户端,可以支持不同编程语言技术栈应用。
下面通过实际代码例子来看看如何编写契约测试,例子中使用Pact-JS,该例子来源于Pact官网。官网的例子中编写了多个demo code,以e2e中的例子为例,e2e下定义了provider服务,如下图中间的截图所示,provider里面定义了一些接口,且provider服务监听在8081端口,其次,是consumer端服务,consumer端的代码实现了调用provider端服务的逻辑,如下图右边截图,consumer端服务监听在8080端口。真正的契约测试编写在test目录下两个文件中,consumer.spec.js和provider.spec.js.
首先来看一下consumer.spec.js文件,测试代码中首先通过new Pact定义了基本信息,consumer,provider名词,后续生成的json格式的Contract文件名称就是consumer和provider的名称组合起来的;接着是port端口,运行consumer端测试时,实际Pact的作用是:根据契约信息启动了一个Mock服务,consumer端此时会调用Mock服务,而不是真实的provider端服务,至于如何用mock服务替换provider端服务稍后介绍,这里的port,定义的就是启动的mock服务端口,后面还有dir定义,即生成的契约文件的folder名称,spec:2,表示总共有2个契约测试Case。
接着看看如果编写契约测试,编写契约测试主要包含两部分,第一部分就是通过Pact提供的matching规则编写接口的Request和Response,对于Get类请求,主要是编写Response,以下图为例,这里使用Like关键字定义了animalBodyExpectation,like表示,生成契约文件中对这个字段只做类型检查,契约测试重点关注的是接口的schema,即接口包含的字段以及字段的类型,不会去关注字段具体的值,因为接口字段值是属于服务端接口测试的范畴。第二部分就是编写consumer端的单元测试了,如右下图所示:在before阶段,通过调用provider.addInteraction({...})方法来定义启动起来的mock服务中,mock的接口的情况,这里state和uponReceiving都是接口的描述信息,withRequest和willResponse定义mock的接口的请求和响应。it(....)部分就是基于consumer端的代码编写单元测试,这里编写了调用consumer端的suggestion()方法,期望返回的结果。
当运行测试,将生成的契约上传到pact broker时,可以看到如下信息,第一个描述信息"A request for all animals given has some animals"就是上图before中定义的provider.addInteraction部分内容,在pact broker上点击超链接,可以跳转查看到pact启动的mock服务中,mock的接口信息的request和response。可以看到Response中每个字段的值和animalBodyExpectation中一致。
前面提到过,在consumer端编写自己的代码时,调用的provider端接口,指定的肯定是provider端服务的baseUrl,那么,在运行consumer端测试时,如何用启动mock服务替换掉真实的provider端服务的呢?在顶层的describe()中,还有一段before()代码,这里调用provider.setup.then()的方法,里面做的事情,就是overwrite了环境变量API_HOST。
继续查看Consumer服务中的代码,发现consumer在调用第三方服务时,也定义了API_HOST这样一个环境变量来指定三方服务的baseUrl。
总结下来,如果要编写契约测试,那么,consumer端,需要将调用的第三方服务的baseUrl抽取到环境变量中,这样在编写契约测试时,只需要overwrite环境变量,即可达到用mock服务替代真实第三方服务的目的。
除了服务替换外,在编写契约测试时,还需要熟练掌握工具提供的Matching规则,因为,使用Pact编写契约测试,对于Provider端来说,会启动真实的服务,校验契约中的接口与真实Provider端的接口是否相同,使用matching时,以校验Type为主,基本不会涉及校验具体的值,因为,对于consumer来说,provider可能是其他团队在负责,他无法提前准备测试数据,所以,契约测试重点是校验schema。那么Pact-JS提供了哪些Matching规则呢?具体如下所示。在校验类型时,可以用like关键字,或者直接写类型,如string,number,boolean,integer等,对于数组类型的信息,可以用eachLike关键字。更多详细信息可看官网。
上面review了consumer端的测试,再来看看provider端的测试,provider端的测试相对比较固定,需要注意一点,new Verifier({...})中provider名称要和consumer中定义的名称一致。另外,是配置pact broker的信息,例如登录的用户名称、密码等,下面配置的是一个public的pact borker信息。
上面从代码层面介绍了如何通过Pact-JS编写契约测试,接着看看如何运行测试,具体步骤如下所示:
1.从官网下载代码
2.安装pact-foundation (npm i -S @pact-foundation/pact@latest)
3.cd到example目录下的e2e目录,安装依赖(npm install)
4.npm run test:consumer (from e2e directory) - Run consumer tests
5.npm run test:publish (from e2e directory) - Publish contracts to the broker
6.npm run test:provider (from e2e directory) - Run provider tests
运行完成后,可以从日志中看到执行结果,结果如下所示:
登录到pact-broker,可以查看pact详细信息,也可以查看provider端与consumer端的network- graph.