有近两周没有在公众号中发表文章了,看过我之前公众号的读者都知道,公众号中近期在连载《RobotFramework接口自动化系列课程》,原本计划每周更新一篇,最近由于博主在带一个新项目,实在是没空抽出时间来,所以向公众号中对连载课程一直期待的读者说声抱歉。
由于最近带微服务的项目,而对于微服务其实也是近从14年才流行起来,对于这块目前的干货内容还是较少,借着机会,小结一下知识点。所以今天也先不打算连载《RobotFramework接口自动化系列课程》,如果读者对连载的课程比较热衷的话,可以在留言板下面给笔者留言,如果读者反馈较多的话,博主也会适当加快调整课程分享节奏。
下面给大家浅聊一下微服务架构下的契约测试。
Microservice微服务是一种架构风格,我们可以把每一个微服务视做一个用一组API提供业务功能的组件,且服务之间会有很多依赖关系,如下图所示:
这些服务之间可能由一个团队或者相互独立的团队开发和维护,并且它们在系统内部相互依赖,在这种情况下,接口的开发和维护可能会带来一些问题,例如服务端调整架构或接口调整而对消费者不透明,导致接口调用失败。
例如, 我们想测试某微服务架构中的某一个服务时,比如下图第一排中间的服务,如:
因为它和其他服务都存在交互,一般我们有两种方式:
下面分析一下这两种方式优缺点:
对比分析 | 优点 | 缺点 |
---|---|---|
第一种方式:部署所有服务 | 1、模拟生成环境 2、可以真实地测试服务交互 |
1、测试其中一个服务,不是不布署全部服务,包括各基础设施 2、要运行很长时间 3、不能及时给予测试反馈 4、测试环境被一个测试服务锁定,别人无法同时使用。 |
第二种方式:Mock其它服务 | 1、测试反馈快 2、没有基础服务依赖要求 |
1、服务的实现方创建的Stubs,可能实现与这个无关 2、无法模拟真实数据交互环境 |
常规我们开发的项目主要由服务提供方约定接口,虽然提供方架构调整或改变接口之前通常会通知消费者,但可能还是会存在遗漏。
当一个Service已经同时被多个使用者调用用的时候,怎么保证service的修改对其它所有使用者造成影响被感知到呢?
契约测试 ,又称之为 消费者驱动的契约测试(Consumer-Driven Contracts,简称CDC),根据 消费者驱动契约 ,我们可以将服务分为消费者端和生产者端,而消费者驱动的契约测试的核心思想在于是从消费者业务实现的角度出发,由消费者自己会定义需要的数据格式以及交互细节,并驱动生成一份契约文件。然后生产者根据契约文件来实现自己的逻辑,并在持续集成环境中持续验证。
后文中消费者驱动的契约测试统一用cdc来代替。
业界常用的CDC测试框架有:
类型 | 描述 |
---|---|
单元测试 | 单元测试针对代码单元(通常是类)的测试,单元测试的价值在于能提供最快的反馈。另外好的单元测试还可以帮助你改善设计,在你的团队掌握TDD的前提下,单元测试能辅助重构,帮助改善代码整洁度。 |
API测试 | API测试是针对业务接口进行的测试,主要测内部接口功能实现是否完整,比如说内部逻辑是不是正常,异常处理是不是正确。 |
契约测试 | 契约测试其实是为了测试服务之间连接或者说接口调用的正确性,为了验证服务提供者的功能是不是真正能够满足消费者的需求。它其实体现了测试前移的思想,把本来要通过集成测试才能验证的工作化作单元测试和接口测试,用更轻量的方式快速进行验证。 |
集成测试 | 它从用户的角度验证整个功能的正确性,测的是端到端的流程,并且加入用户场景和数据,验证整个过程是不是OK,它的价值业务价值最高,是验证一个完整的流程。 |
注:通常在工程实践上,当消费者根据需要生成了契约之后,我们会将契约上传至一个公共可访问的地址,然后生产者在执行时会访问这个地址,并获得最新版本的契约,然后对着这些契约来执行相应的验证过程。
基于消费者的业务逻辑,驱动出契约
其实现步骤如下所示:
1、使用Pact的DSL,定义Mock提供者,如localhost:8080
2、将Mock地址传给消费者并对Mock的提供者发送请求。
3、使用Pact的DSL,定义响应内容(包括Headers、Status以及Body等)。
4、在消费者端 使用@PactVerification运行单元测试(Pact集成了JUnit、RSpec等框架),生成契约文件。
5、当运行测试后,Pact框架记录消费者的名称、发送的请求、期望的响应以及元数据,将其保存为当前场景下的契约文件,通常命名为[Consumer]-[Provider].json,例如 orderConsumer-orderProvider.json
6、契约文件生成后,我们可以将其保存在文件系统或者Pact-Broker(Pact提供的中间件,用来管理契约文件)中,以便后续提供者使用。
基于消费者驱动出的契约,对提供者进行验证
在提供者端,我们不需要写任何验证的相关代码,Pact已经提供了验证的接口,我们只需要做好如下配置:
1、为提供者指定契约文件的存储源(如文件系统或者Pact-Broker)。
2、启动提供者,运行PactVerify(Pact有Maven、Gradle或者Rake插件,提供pactVerify命令)。
3、当执行pactVerify时,Pact将按照如下步骤,自动完成对提供者的验证:
4、构建Mock的消费者。
5、根据契约文件记录的请求内容,向提供者发送请求。
6、从提供者获取响应结果。
7、验证提供者的响应结果与Pact契约文件定义的契约中是否一致。
传统情况下做集成测试需要把服务消费者和服务提供者两个服务都启动起来再进行测试,而Pact做契约测试时将它分成两步来做,每一步里面都不需要同时启动两个服务。
测试解耦,就是服务消费与提供者解耦,甚至可以在没有提供者实现的情况下开始消费者的测试。
一致性,通过测试保证契约与现实是一致性的。
测试前移,可以在开发阶段运行,并作为CI的一部分,甚至在开发本地就可以去做,而且可以看到一条命令就可以完成,便于尽早发现问题,降低解决问题的成本。
Pact提供的Pact Broker 可以自动生成一个服务调用关系图,为团队提供了全局的服务依赖关系图。
Pact提供Pact Broker这个工具来完成契约文件管理,使用Pact Broker后,契约上传与验证都可以通过命令完成,且契约文件可以制定版本。
降低服务间的集成测试成本,尽早验证当提供者接口被修改时,是否破坏了消费者的期望。
目前仅支持采用REST通信协议。
以上部分转载于:https://testerhome.com/topics/10806
Pact实战
consumer代码实例:
•Maven依赖
<dependency>
<groupId>au.com.diusgroupId>
<artifactId>pact-jvm-consumer-junit_2.11artifactId>
<version>3.5.21version>
<scope>testscope>
dependency>
代码:
/**
* Created by jackeymm on 2018/9/7.
*/
@RunWith(SpringRunner.class)
public class DomainConsumerPactTest {
@Rule
public PactProviderRuleMk2 stubProvider = new PactProviderRuleMk2("domain", this);
@DefaultRequestValues
public void defaultRequestValues(PactDslRequestWithoutPath request){
request.headers(singletonMap(CONTENT_TYPE, APPLICATION_JSON_VALUE));
}
@DefaultResponseValues
public void defaultResponseValues(PactDslResponse response){
response.headers(singletonMap(CONTENT_TYPE, APPLICATION_JSON_VALUE));
}
@Pact(consumer = "dispatcher")
public RequestResponsePact createDomainNotExistPact(PactDslWithProvider builder) throws JSONException{
return builder
.given("CreateDomain - domain does not exist")
.uponReceiving("Normal request")
.path("/domain/create")
.method(HttpMethod.POST.name())
.body(new JSONObject()
.put("domain","a.com")
)
.willRespondWith()
.status(OK.value())
.body(new PactDslJsonBody()
.numberValue("code", 0)
.object("data")
.integerType("version")
.closeObject()
)
.toPact();
}
@Pact(consumer = "dispatcher")
public RequestResponsePact createIllegalRequestPact(PactDslWithProvider builder) throws JSONException {
return builder
.given("CreateDomain - Illegal request param")
.uponReceiving("Illegal request")
.path("/domain/create")
.method(HttpMethod.POST.name())
.body(new JSONObject()
.put("pubKey","pk")
.toString()
)
.willRespondWith()
.status(BAD_REQUEST.value())
.body(new PactDslJsonBody()
.numberValue("code", BAD_REQUEST.value())
.stringType("message","param is invalid")
)
.toPact();
}
@Test
@PactVerification(fragment = "createDomainNotExistPact")
public void verifyCreateDomainNotExistPact() throws Exception {
given()
.contentType(APPLICATION_JSON_VALUE)
.body(new JSONObject()
.put("domain","a.com")
.toString()
)
.when()
.post(stubProvider.getUrl() + "/domain/create")
.then()
.statusCode(OK.value())
.body("code", is(0));
}
@Test
@PactVerification(fragment = "createIllegalRequestPact")
public void verifyCreateIllegalRequestPact() throws Exception{
given()
.contentType(APPLICATION_JSON_VALUE)
.body(new JSONObject()
.put("pubKey","pubk1")
.toString()
)
.when()
.post(stubProvider.getUrl() + "/domain/create")
.then()
.statusCode(BAD_REQUEST.value())
.body("code", is(BAD_REQUEST.value()));
}
}
•Pact文件发布
ØMaven依赖
<plugin>
<groupId>au.com.diusgroupId>
<artifactId>pact-jvm-provider-maven_2.12artifactId>
<version>3.5.21version>
<configuration>
<pactBrokerUrl>http://127.0.0.1:8888/pactBrokerUrl>
configuration>
plugin>
Ø运行命令
mvn clean test pact:publish -Dtest=com.domain.DomainConsumerPactTest
provider代码示例:
•Maven依赖
<dependency>
<groupId>au.com.diusgroupId>
<artifactId>pact-jvm-provider-junit_2.11artifactId>
<version>3.5.21version>
<scope>testscope>
dependency>
<dependency>
<groupId>au.com.diusgroupId>
<artifactId>pact-jvm-provider-spring_2.11artifactId>
<version>3.5.21version>
<scope>testscope>
dependency>
代码:
/**
* Created by jackeymm on 2018/9/7.
*/
@RunWith(SpringRestPactRunner.class)
@Provider("domain")
@PactBroker(host = "127.0.0.1", port = "8888")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class DomainProviderPactTest {
@TestTarget
public final Target target = new SpringBootHttpTarget();
@MockBean
private DomainMapper domainMapper;
@State("CreateDomain - domain does not exist")
public void runDomainNotExistState(){
when(domainMapper.save(any(Domain.class)))
.thenReturn(1);
}
@State("CreateDomain - Illegal request param")
public void runDomainIllegalState(){
}
}