背景

如今,契约测试已经逐渐成为测试圈中一个炙手可热的话题,特别是在微服务大行其道的行业背景下,越来越多的团队开始关注服务之间的契约及其契约测试。

什么是契约测试

    关于什么是契约测试这个问题,首先先看一下Pact官方文档给出的定义:pact的官方文档,是另一个可以帮助我们理解契约测试的地方。它对契约测试给出了这样的定义:"Contract testing is a way to ensure that services (such as an API provider and a client) can communicate with each other"。这里面需要关注的重点是"communicate ",它给出了Pact对契约测试范畴(scope)的定义。契约测试又称之为 消费者驱动的契约测试。这里的契约是指软件系统中各个服务间交互的数据标准格式,更多的指消费端(client)和提供端(server)之间交互的数据接口的格式。

契约测试的价值

那什么是契约测试的价值呢?要说清楚契约测试的价值,就需要准确认识契约测试的精髓——"消费者驱动"

在讨论契约测试的范畴里,”消费者驱动”述及的对象是契约,而不是契约测试。所以谁被驱动的对象就是契约。举个例子,当某个provider正常上线后,某个consumer需要消费这个provider的服务,那么应该由consumer来提出期望来建立它们之间的契约测试。因为,契约测试,形式上,虽然测试的是provider,但是在价值上,保证的却是consumer的业务。如果消费者对自己都不上心, 那你也不要指望生产者能操什么心。这些都是在跨团队的微服务体系下真切的痛点。 这里举一个契约测试的经典案例:

在上图一个简单的消费关系中,provider为consumer A,B,C提供服务。provider提供的结构包含name、age和gende三个简单的字段。这份包含name、age和gender的JSON,其本身只是一个schema,并不是任何契约。因为契约一定是成对存在的,没有确切consumer的交互定义,只是schema,不是契约。

    如上图有三个消费者,并且消费的字段各不相同,所以这里需要有三份契约(对应的,也需要三份契约测试)。

  • consumer A消费page和gender,
  • consumer B消费name、age和gender;
  • consumer C消费name和gender

就目前provider提供的schema来说,没有任何问题,大家相安无事。但是某日因为业务需求,consumer C期望provider提供更加详细的name信息,包括firstName和lastName。这个需求对provider是小case,所以,provider打算对schema做类似下面的修改:

 

  这样的修改,很明显对consumer C是需要的,对consumer A无所谓,但对consumer B却是不可接受的,属于典型的契约破坏。此时,provider和consumer B之间的契约测试就会挂掉,从而对provider提出预警(至于,剩下的,怎么协调和consumer B的兼容问题,就不是契约测试关注的问题,那需要的是团队间的交流)上面这个示例中的一些细节,可以帮助我们发掘契约测试的两大价值点

1. 应对单个provicder多个consumer

要最大化的体现契约测试异于集成测试的价值,一定是在"单个provider对应多个consumer"的架构下来说的。因为,在只有一个provider和一个consumer的架构下,只存在一份契约,对该契约内容的任何修改,对这对provider和consumer来说,都是显而易见的,那么就不会出现契约破坏的情况。在这种情况下,集成测试往往就已经完整的达到了契约测试的目的。

  但是在单个provider对应多个consumer的架构下,情况就不一样了在上文的例子中provider和consumer C之间的契约修改,对consumer A无影响,对consumer B却是契约破坏,对这种情况,集成测试是无能为力的。在上边例子中,有4个service,所以就会有4个集成测试,每个集成测试只会关注自己的业务正确性,provider修改后,只有consumer B的集成测试会挂掉。但那都是在provider的契约破坏生效之后的事情了。可见,虽然4个集成测试都各司其职,但都不能对这个契约破坏的问题做到防患于未然!只有契约测试,才是这个问题的最佳答案!这就是契约测试最大的价值,它只会在"单provider多consumer"的环境下(这是微服务的常见场景,但不是必然场景),才能发挥出来。

2.减少团队沟通成本  

  真正的业务场景下,特别是一些复杂的微服务集群,又或者是一些时间跨度很长的系统,对于某个provider,到底有多少个consumer?而provider的每一处修改,又对哪些consumer的契约造成怎样的影响?这些往往都是很难确定的问题。当在集团业务中一个provider有十几个 consumer时,每次provider要更新,就得八方去通知这些consumer的团队来做回归测试。有时,一点小小的修改,回归测试一分钟就可以搞定,但人肉联系各个团队却会花上好几天。如果每个consumer都能和provider建立契约测试(这里我们暂且不考虑负载和去重的问题),我们就能很好的解决这些效率问题。 当需要把provider的schema中的一个String改成Object,从契约的角度,我们还在纠结如何协调所有的consumer影响最小时,最简单的一个解决方案就是“不把String改成Object,而是直接添加这个Object"

契约测试和功能测试区别

首先这里的功能测试是指接口测试和集成测试, 学习契约测试的时候一定要弄清楚契约测试和功能测试)之间的区别。契约测试主要是用于以下几点

  • 测试接口和接口之间的正确性
  • 验证服务层提供的数据是否是消费端所需要的
  • 将本来需要在集成测试中体现的问题前移,更早的发现问题
  • 更快速的验证消费端和提供端之间交互的基本正确性

根据契约测试的用途我们可以发现契约测试和功能测试之间的区别如下:

     1. 功能测试关注的是provider的正实现其设计,契约测试关注的是provider的实现(也包括设计)是否满足每一个consumer的需求。注意,功能测试只关注provider自身,而契约测试则关注每一个consumer;
     2. 功能测试的测试案例,由provider的团队提供,契约测试的测试案例,基于消费者驱动,由各个consumer团队提供;
     3. 一个provider只会有一个功能测试,但契约测试,理论上,可以无限,有多少consumer就可以有多少个契约测试;
     4. 集成测试的测试对象是一定是consumer,或者说是一个服务作为consumer的角色(因为,某个服务经常既是consumer,又是provider,而契约测试的被测试对象一定是provider;

Example by Pact

Pact最早是用Ruby实现的,目前已经扩展支撑Java,.NET,Javascript,Go,Swift,Python和PHP。 这里我使用springboot+PACT+gradle搭建契约测试。

1.添加依赖

  在项目的build.gradle文件中添加如下依赖

buildscript {
ext {
pactVersion = "4.0.2"
kotlin_version=1.3.50
}
dependencies {
classpath("au.com.dius:pact-jvm-provider-gradle:${pactVersion}")
}
} apply plugin: "au.com.dius.pact"
dependencies {
testImplementation "au.com.dius:pact-jvm-consumer-junit:${pactVersion}"
testImplementation "au.com.dius:pact-jvm-consumer-java8:${pactVersion}"
}

这里有几个注意点:

1. 由于这里用的pact的版本是4.0.2的,pact插件中依赖的kotlin版本是1.3.50,所以项目中kt的版本也要是1.3.50,然而springboot现在默认自己管理kt的版本,目前项目中采用的springboot是2.1.10.RELEASE默认使用的kt是1.2.7,会导致pact加载失败,所以要自己手动指定kotlin版本

2.有写时候引入的依赖包中可能会指定pact版本,而且是低版本的这个时候也要注意版本冲突。

2.编写测试用例

想编写一套完整的PACT测试用例一般分为以下四步

1.确定consumer需求

契约测试编写测试用例,首先要素是根据consumer需求编写,这里我假设consumer需求如下

  • Get请求:http://ip:port/test/{id}?type=test
  • 要求返回的响应 
    {
    "id": "a7a1b044-b8a8-4ef9-ae1b-00599f2281cc",
    "data": {
    "type": "test",
    "projectName": "testadmin",
    "machineCode": "1111",
    "validity": "2022-01-18 23:59:59.0",
    "useNum":500
    }
    }

2.编写consumer测试代码

  根据consumer的需求编写Test case

@Test
public void testWithQuery() { // 构造consumer需要验证的响应内容
DslPart body = newJsonBody((root) -> {
root.stringType("id");
// 对应上文的data结构
root.object("data", (dataObject) -> {
// 验证返回的type的值是否是"test"
dataObject.stringValue("type", "test");
//验证类型是否为string
dataObject.stringType("projectName", "tesadmint");
dataObject.stringType("machineCode");
dataObject.timestamp("validity");
dataObject.numberType("userNum", 500);
});
}).build(); RequestResponsePact pact = buildPactResponse("test",5,body); MockProviderConfig config = MockProviderConfig.createDefault(PactSpecVersion.V3);
PactVerificationResult result = runConsumerTest(pact, config, (mockServer, context) -> {
// 自己的客户端调用服务
TestRestService testRestService = new TestRestService ();
// 生成一个mockServer,代替服务端返回响应
TestResponse testResponse= TestResponse
.fetchLicenseGetId(mockServer.getUrl(), "/test/1?testType=test");
// 返回的响应内容
TestData testdata= testResponse.getData();
// 验证响应内容
assertEquals(testdata.getType(), "test");
return null;
});
checkResult(result);
} // 返回响应的请求头类型
private RequestResponsePact buildPactResponse(String testType, int id,DslPart body) {
Map<String, String> headers = new HashMap<String, String>();
headers.put("Content-Type", "application/json;charset=UTF-8");
return ConsumerPactBuilder
.consumer("TestGetIdConsumer")
.hasPactWith("TestLicProvider")
.given("")
.uponReceiving("Query " + tesType + " lic is " + id)
.matchPath("/lic/[0-9]+", "/test/" + id)
.query("testType=" +testType)
.method("GET")
.willRespondWith()
.headers(headers)
.status(200)
.body(body)
.toPact();
}

其中TestRestService 是consumer应用代码中的类,我们直接使用它来发送真正的Request,发给谁呢?发给mockServer,Pact会启动一个mockServer, 基于Java原生的HttpServer封装,用来代替真正的Provider应答createPact中定义好的响应内容,继而模拟了整个契约的内容。

编写测试类我这里使用的是Junit DSL方式,这种方式可以在一个测试类中编写多个测试方法而基本的Junit和Junit Rule的写法只能在一个测试文件里面写一个Test Case。当然,Junit DSL的强大之处绝不仅仅是让你多写几个Test Case,通过使用PactDslJsonBody和Lambda DSL你可以更好的编写你的契约测试文件:

    • 对契约中Response Body的内容,使用JsonBody代替简单的字符串,可以让你的代码易读性更好;
    • JsonBody提供了强大的Check By Type和Check By Value的功能,让你可以控制对Provider的Response测试精度。比如,对于契约中的某个字段,你是要确保Provider的返回必须是具体某个数值(check by Value),还是只要数据类型相同就可以(check by type),比如都是String或者Int。你甚至可以直接使用正则表达式来做更加灵活的验证;
    • 目前支持的匹配验证方法请参考官方文档,这里不多说

3.设置契约生成目录

在build.gradle中添加契约文件存放地址

test {
systemProperties['pact.rootDir'] = "${buildDir}/Pacts/"
}

4. 运行测试类  

在junit中运行clean test,运行成功后会生成在对应目录下契约文件。这里我用的idea,所以直接运行gradle task即可

Provider测试

comsumer端测试代码编写完毕,契约也生成好了,接下来就是要执行Provider端测试了,要想执行Provider测试,首选要获取consumer端的契约文件;契约文件,也就是上文Pacts目录下面的那些JSON文件,可以用来驱动Provider端的契约测试。由于我们的示例把Consumer和Provider都放在了同一个codeBase下面,所以Pacts下面的契约文件对Provider是直接可见的,而真实的项目中,往往不是这样,你需要通过某种途径把契约文件从Consumer端发送给Provider端。Pact提供了更加优雅的方式那就是使用Pact Broker。目前有好些方法可以搭建Broker服务,我推荐使用Docker来个一键了事。

要想发布到Broker上,需要配置发布地址,在gradle中加入如下配置

pact {
publish {
// 契约地址
pactDirectory = "${buildDir}/${pactPath}/"
//broker url
pactBrokerUrl = mybrokerUrl
}
}

这里搭建的broker不需要用户名和密码, 所有无需配置用户名和密码。配置完后运行发布任务pactpublish,如果是idea的话在右边是可以直接找到发布的task,双击便可执行

发布成功后在broker上可以看到consumer的契约文件。如下

consumer发布契约成功后,provicer就可以从broker上拉取契约文件了,在build.gradle的pact Task中添加serviceProviders配置

pact {
publish {
pactDirectory = "${buildDir}/${pactPath}/"
pactBrokerUrl = mybrokerUrl
}
serviceProviders {
SignLicProvider {
protocol = 'http'
host = 'localhost'
port = 8880
path = '/' // Test Pacts from local
hasPactWith('') {
pactSource = file("${buildDir}/${pactPath}/TestGetIdConsumer-TestProvider.json")
} // Test Pacts from Pact Broker
hasPactsFromPactBroker(mybrokerUrl)
}
}
}

如果想把provider端测试的结果提交到broker上,需要开启结果上传配置。 在build.gradl中 添加 pact_verifier_publishResults=true 即可。添加成功以后,就可以执行provider端的契约测试了。在执行provider端的测试之前,要先保证provider端的服务开启,否则无法工作。在idea中的task中可以找到新增的对应的task:pactVerify_TestProvider,然后双击运行。也可以手动执行task:TestProvider:pactVerify。

task执行成功后控制台会显示契约测试是否执行通过,或者在broker上也可以看到最近提交的结果。

最后我们就可以根据契约测试的结果,进行沟通或者修改了。

总结

一般契来说约测试是在单元测试之后,集成测试之前要进行的,首先在保证各自功能正确的前提下测试消费者和提供者的契约是否相匹配,然后再进一步的测试功能的完备性和整个业务流的正确性。

       从上文可以得出契约测试可以解决如下问题:
  1. 可以使得消费端和提供端之间测试解耦,不再需要客户端和服务端联调才能发现问题
  2. 完全由消费者驱动的方式,消费者需要什么数据,服务端就给什么样的数据,数据契约也是由消费者来定的
  3. 测试前移,越早的发现问题,保证后续测试的完整性
  4. 通过契约测试,团队能以一种离线的方式(不需要消费者、提供者同时在线),通过契约作为中间的标准,验证提供者提供的内容是否满足消费者的期望

关于契约测试本身,和契约测试实施的问题,我想,远不止上面诉及的方面。不同的人、不同的团队,对契约测试的理解也可能都不一样,当一种新的理念在不同的现实项目中付诸实践时,可能遇到的问题,和思考的方式又会有所迥异,这些都是我们理解一种理念的正常途径。

参考链接

https://github.com/pact-foundation/pact_broker

https://github.com/DiUS/pact-jvm/tree/master/consumer/pact-jvm-consumer-junit

https://www.jianshu.com/p/ca82cde5b125

基于Pact的契约测试的更多相关文章

  1. 微服务下的契约测试(CDC)解读

    1. 前言 有近两周没有在公众号中发表文章了,看过我之前公众号的读者都知道,公众号中近期在连载<RobotFramework接口自动化系列课程>,原本计划每周更新一篇,最近由于博主在带一个 ...

  2. 契约测试框架-Pact实践

    在前一篇博客中我们讲到契约测试是什么,以及它能给我们软件交付带来什么价值,本次将介绍一个开源的契约测试框架Pact,它最初是用ruby语言实现的,后来被js,C#,java,go,python 等语言 ...

  3. 为什么要抛弃Pact?如何快速实现契约测试(CDC)

    前言 在前几天的博客中,我转载了一篇文章,其中介绍了契约测试和pact是怎么实施的,的确很有帮助.但我经过研究,其实是pact本身也是有缺陷的,结合我近期在使用的服务型工具和我的实际情况,觉得实现契约 ...

  4. 基于soapUI构建WebService测试框架

    基于soapUI构建WebService测试框架 http://www.docin.com/p-775523285.html

  5. 强大的API测试工具Hitchhiker v0.9 基于UI的断言测试,回顾2017

    v0.9是Hitchhiker在2017农历年的最后一个版本,而起点正是刚过完2016农历年,农历2018即将到来,一年轮回,今天写点东西稍微回顾下hitchhiker的2017. 先还是说v0.9, ...

  6. ELM:ELM基于近红外光谱的汽油测试集辛烷值含量预测结果对比—Jason niu

    %ELM:ELM基于近红外光谱的汽油测试集辛烷值含量预测结果对比—Jason niu load spectra_data.mat temp = randperm(size(NIR,1)); P_tra ...

  7. 转:基于Jmeter的MQTT测试插件

    基于Jmeter的MQTT测试插件-上 1. Jmeter插件简介 Apache JMeter是Apache组织开发的基于Java的压力测试工具.下载 用于对软件做压力测试,它最初被设计用于Web应用 ...

  8. 《软件测试自动化之道》读书笔记 之 基于反射的UI测试

    <软件测试自动化之道>读书笔记 之 基于反射的UI测试 2014-09-24 测试自动化程序的任务待测程序测试程序  启动待测程序  设置窗体的属性  获取窗体的属性  设置控件的属性  ...

  9. 《软件测试自动化之道》读书笔记 之 基于Windows的UI测试

    <软件测试自动化之道>读书笔记 之 基于Windows的UI测试 2014-09-25 测试自动化程序的任务待测程序测试程序  启动待测程序  获得待测程序主窗体的句柄  获得有名字控件的 ...

随机推荐

  1. Nebula 架构剖析系列(二)图数据库的查询引擎设计

    摘要 上文(存储篇)说到数据库重要的两部分为存储和计算,本篇内容为你解读图数据库 Nebula 在查询引擎 Query Engine 方面的设计实践. 在 Nebula 中,Query Engine ...

  2. 领扣(LeetCode)删除注释 个人题解

    给一个 C++ 程序,删除程序中的注释.这个程序source是一个数组,其中source[i]表示第i行源码. 这表示每行源码由\n分隔. 在 C++ 中有两种注释风格,行内注释和块注释. 字符串// ...

  3. ZeroC ICE的远程调用框架 Slice如何帮助我们进行Ice异步编程(AMI,AMD)

    Slice最大的用处就是为我们使用Ice进行编程,代劳绝大部分的重复性代码,并提供一些帮助性的框架代码,如用于AMI和AMD方式进行异步编程的回调框架. 当Slice不为我们生成代码时,我们仍然可以按 ...

  4. 构建之法组——“别吃错喽”微信小程序评价

    此作业要求参见https://edu.cnblogs.com/campus/nenu/2019fall/homework/9860 基于NABCD评论作品,及改进建议 1.根据(不限于)NABCD评论 ...

  5. 20190926-2 选题 Scrum立会报告+燃尽图05

    此作业要求参见:https://edu.cnblogs.com/campus/nenu/2019fall/homework/8678 一.小组情况组长:迟俊文组员:宋晓丽 梁梦瑶 韩昊 刘信鹏队名:扛 ...

  6. 程序员常用6 个 Python 的日期时间库

    内建的 datetime 模块 在跳转到其他库之前,让我们回顾一下如何使用 datetime 模块将日期字符串转换为 Python datetime 对象. 假设我们从 API 接受到一个日期字符串, ...

  7. pyinstaller打包python文件成exe(原理.安装.问题)

    py文件打包成exe文件的方式一共有三种:py2exe.PyInstaller和cx_Freeze 本文分四个步骤来详讲如何用PyInstaller将py文件打包成exe文件 1. PyInstall ...

  8. 探索 IPv6 网络

    目录 0x00 前言 0x01 探索 服务器配置 IPv6 地址 服务器部署网络代理 客户端配置网络代理 测试访问 IPv6 地址 给博客添加 IPv6 地址 0x00 前言 IPv4 地址枯竭的事情 ...

  9. day 23 复习

    本来应该学习day23,由于上午未学习,下去困,导致今天未进行进度 那就做一下简单的复习吧! 1. while else结构,如果while 后的条件条件不再满足 引发循环再继续,则执行else中的内 ...

  10. 5分钟上手自动化测试——Airtest+Poco快速上手

    版权声明:该文章为AirtestProject原创文章:允许转载,但转载必须注明“转载”并保留原链接 前言 本文档将演示使用`AirtestProject`专用的编辑器AirtestIDE,编写`Ai ...