笔者的经验认为,微服务的出现,是为了应对传统SOA架构在多服务背景下的疲软,本质上是SOA的进一步衍生,是一种治理服务的手段。而微服务解决得了传统SOA、单块大单体程序的问题,依赖于微服务自身的健壮性、灵活性、可扩展性和持续敏捷,这些特性是通过持续集成、持续交付来落实的。而持续集成、持续交付的逻辑前提是:应对变化(主动也罢、被动也罢)时所开展的持续修改、完善需要尽量的顺滑,避免各种各样的问题导致卡停。这些问题主要来源于软件开发的老短板,比如修改和扩展后代码的有效性、可信度。新的功能需要快速被验证而不是等待业务测试人员手工的一环又一环的测试验证。已经改过的代码,并不至于原有的功能或服务失效、错误,如果有应尽早发现,尽早解决。这就需要自动测试才可能支撑真正有效果、有效率的持续集成、持续交付。做项目也罢,开发软件也罢,都是有问题和缺陷的,尽早发现,尽早处理,这就是自动测试的价值和意义,也决定了自动测试实际上是持续集成、持续交付、有质量的微服务的重要内容和基石。在我以前的很多项目中,往往一到后期,就是各种BUG在SIT\UAT阶段纠缠,这是因为前面阶段部分甚至完全放弃了开发人员的自测而将所谓希望放在后续测试人员的手工测试上(效率很低、扯皮严重)。这种项目的痛苦,只有靠心大来应对,慢慢的走向漫不经心和团队崩塌,这不该是技术工作可以有和应该有的样子。正因如此,在读到《敏捷革命》一书时,对杰夫·萨瑟兰他老人家所说的想要找到方法以“指导团队变得更高效、更愉快、更具有相互扶持精神、更有乐趣以及更加令人向往。”深以为然。这个方法是持续集成、持续交付,而基石和新的梁柱手段就是自动测试,为了“更开心、更越快的工作”。
事实上,不管是《领域驱动设计》、还是《微服务架构设计》、《敏捷革命》等书以及马丁·福勒老爷子都一直在强调自动测试,这不是巧合,而是因为自动测试确实是当下软件行业进化的一个重要手段。通过自动测试才可能实现小步快跑,小问题小修,大问题重构,使得机会、资源和精力合理匹配。自动测试事实上很早就已经有各种框架支持,对于java语言而言就是junit,对于微服务而言比较主流的就是spring cloud contract。
spring cloud contract 是spring cloud的组件,不过完全可以与eruake这类服务发现等核心组件分开看待,单独作为服务的集成测试框架来使用。事实上单元测试而言,不需要contract支持。contract的存在意义和价值在于集成测试和组件测试(验收测试一般需要最接近真实生产的完整链条背景下的端到端测试,这个环节反倒是维持传统的人员手工测试更合理些)。事实上,contract也是有效实现所谓测试驱动开发(TDD)的一种框架。contract的本质就是为服务的消费者(我喜欢称之为服务请求者、客户端client)提供后台服务的模拟(stub),也可以为服务的提供者(服务端 server)提供前端请求的模拟(mock)。contract的中文含义是契约,这就是经常会听到的“契约编程”的一种实质落地,也就是服务提供者和服务消费者,坐下来谈判和讨论,根据消费者的需求、提供者的条件和承诺(也许因为提供者太忙,就是懒得管消费者的需求,只给用现有接口,最多给你的接口定义文档和调试支持,爱用不用。也就是DDD中所谓的conformist-跟随者模式,所以这个谈判是必然的)形成服务的协议,其实就是接口的定义(一个请求和响应的例子,包括通信方式、消息格式、消息组成及其意思和确切的内容)。在contract框架中,这个接口需要由双方协商确定后,编写一个定义文件(我喜欢叫做contract脚本文件)。一般是grovvy语言的文件,也可以是yaml的。关于该文件是由服务提供者还是服务请求者编辑,其实都可以,看合作模式和文化了。有关细节在后续的内容中,我再详细描述。基于这个contract脚本文件,contract框架可以生成一个stubs(桩),这个桩的目的在于给服务请求者提供后台服务接口的模拟(stub)条件。如何生成,以及如何让这个桩工作起来,后续内容我在详细描述。有了这个桩,服务请求者就可以在test类中编辑测试方法中 通过模拟调用遵守contract定义的接口的内部服务A,从而既验证调用的方式是否正确,又基于模拟响应的内容从而对内部服务A的实现进行验证(比如对响应回来的数据进行在加工以后的结果是否符合预期),基于测试方法中的断言,程序在编译打包时会提示有关的实现是否正确,从而能够在此阶段就发现和修正错误,避免BUG沉淀到后面才暴露出来。(另:由于篇幅和复杂度的控制,本章只讲述服务消费者一侧的意义和使用,服务提供者一侧在以后的文章中提供)
下面是我所理解的过程序列和概念示意图来表达这个过程的逻辑。
事实上,在以前我自己利用python开发过一个挡板程序,用于支持集成测试,作为模拟的服务提供者,基本逻辑也是和contract差不多,不过在编码上不一样,而且定位还是更倾向于挡板,不具备支持契约化开发的能力。这也体现了,微服务而言,由于采取了进程间通信、接口化对接,从而在开发语言和运行平台上自然而然的是不排他的。旁话不说了,下面开始介绍,我根据官网的文档开发的一个例子,并说明一下其中踩过的坑和细节上的理解。
1,编制契约,创建stub桩
在官网还是很多其他网友的文章中,关于contract脚本由谁来编制,似乎并没有一个统一的说法。在《微服务设计架构模式》中,作者的提倡是在服务消费者权利比较大,可以驱使服务提供者的背景下(也就是),提倡由服务消费者团队来开发contract然后把版本提供给服务提供者。事实上,也有些地方提倡由服务提供者来编制contract(契约-模拟),因为如果服务者团队具有更大的权威或者风险管控压力的话,那就不可能把主动权和版本修订权提供给服务请求者的团队。下面的例子,我是按照官网的做法开展的工作。
以下代码的基本假设和约定:服务提供者、服务请求者都是基于spring boot进行开发的Java应用,通过maven进行制品(或者说构件)管理
假设服务请求团队在与服务提供者图案讨论后,确定了接口的输入输出(也就是服务的需求规格),通信基于HTTP,REST API风格的同步响应模式(事实上contract也支持消息队列等其他格式,不过鉴于控制学习的复杂度,我们放弃在这里讨论学习其他通信方式)。
首先,消费者团队将服务提供者的工程下载到本地(通过GIT工具访问服务提供者工程的库取得,或者其他野蛮一点的拷贝工程代码然后maven指定本地位置的方式都可以)。然后在该工程的src/test/resource/contract/目录中(此位置可以通过在application.yml等配置文件中修改参数contractsDslDir来改变,不设置的话默认是这个目录)创建一个contract脚本文件,该文件的名字不重要可以随意取,比如:service_sample_contract.grovvy
package contracts
org.springframework.cloud.contract.spec.Contract.make {
request {
method 'PUT'
url '/fraudcheck'
body([
"client_id": $(regex('[0-9]{10}')),
loanAmount: 99999
])
headers {
contentType('application/json')
}
}
response {
status OK()
body([
checkStuats: "FRAUD",
"reason": "Amount too high"
])
headers {
contentType('application/json')
}
}
}
说明:本实例是参照官网start文档开发,绝大部分代码也是参照官问编写。因而,具体的业务领域模型也如原网所述,即一个关于某笔贷款是否过大(too high)进行判断并以此认定是否欺诈(FRAUD)嫌疑的业务服务。
如上所示,可以看出脚本本质上就是明确定死请求和响应的内容,可以在HTTP的方法、头和体等元素中进行定制,和我们自己开发一个外部挡板没有太大区别。
到此,为了创建一个stub(测试模拟,桩)所需要进行的编码就完成了。相比其他文章里面的更神秘的流水描述,我倾向于特别声明一下,桩的建立所需要的编码确实就完成了。为了创建这个stub接下来需要做的工作就是修改POM.XML文件,将contract插件置入工程中。
首先在build元素中加入 contract 的maven插件和打包依赖
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>${spring-cloud-contract.version}</version>
<extensions>true</extensions>
<configuration>
<!-- <packageWithBaseClasses>bocd.com.cn.contract-sample</packageWithBaseClasses> -->
<baseClassForTests>toni.com.cn.BaseTestClass</baseClassForTests>
</configuration>
<!-- if additional dependencies are needed e.g. for Pact -->
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-pact</artifactId>
<version>${spring-cloud-contract.version}</version>
</dependency>
</dependencies>
</plugin>
然后,由于contract本质上需要依赖cloud的支撑,工程需要专门加入cloud的dependency
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud-release.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
这里所使用的版本如下:
<properties>
<spring-cloud-release.version>2021.0.3</spring-cloud-release.version>
<spring-cloud-contract.version>3.1.3</spring-cloud-contract.version>
</properties>
需要特别再重申的是,以上的代码和配置修改,都不是服务消费者一侧的工程,而是在服务提供者一侧的工程中。
所以,服务消费者团队需要有服务提供者一方的代码或者说需要有该工程的版本访问权限,比如GIT库的访问权。很多文章中没有强调这一点,也许是因为很多时候开发前后端并没有隔离,就是相同的两三个人,个个都是root型王者,没有权限问题。这是有问题的,因为微服务的本质是把复杂度相互隔离开,把复杂分解下来,这样就可以把权利和责任分派出去。如果没有彼此,耦合度必然会有意无意的越来越紧,混淆在一起。当然,也有可能服务消费者团队根本不具备权限,其实也可以做到,这我在后面会提到。
然后,在该工程下,执行 mvnw clean install -Dmaven.test.skip=true 就可以生成stubs.jar包了。
生成成功的话,可以看到类似如下的console输出:
INFO] Installing D:\workspace\normal\contract-sample-server\target\contract-sample-server-0.0.1-SNAPSHOT.jar to D:\mavenrepo\repository\toni\com\cn\contract-sample-server\0.0.1-SNAPSHOT\contract-sample-server-0.0.1-SNAPSHOT.jar
[INFO] Installing D:\workspace\normal\contract-sample-server\pom.xml to D:\mavenrepo\repository\toni\com\cn\contract-sample-server\0.0.1-SNAPSHOT\contract-sample-server-0.0.1-SNAPSHOT.pom
[INFO] Installing D:\workspace\normal\contract-sample-server\target\contract-sample-server-0.0.1-SNAPSHOT-stubs.jar to D:\mavenrepo\repository\toni\com\cn\contract-sample-server\0.0.1-SNAPSHOT\contract-sample-server-0.0.1-SNAPSHOT-stubs.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
事实上生成的是两个jar包,一个是服务本身的jar包,一个是桩stubs.jar包。本例子工程的基本信息如下:
<groupId>toni.com.cn</groupId>
<artifactId>contract-sample-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>contract-sample-server</name>
因此,生产的包是:contract-sample-server-0.0.1-SNAPSHOT.jar 和 contract-sample-server-0.0.1-SNAPSHOT-stubs.jar 落地的位置就在本地的maven库目录下,如我的本地maven库就在settings.xml文件中定义的位置:<localRepository>D:/mavenrepo/repository</localRepository>。因而,生成的目标的两个包就在“D:\mavenrepo\repository\toni\com\cn\contract-sample-server\0.0.1-SNAPSHOT”下。了解这个机制很重要,后面会说明。
需要注意的地方是,以上编译打包的指令中专门加入了“-Dmaven.test.skip=true”,这一点不能漏掉,因为stub的实现不依赖于任何测试类的存在,但是如果不省略测试过程的话,会在install阶段失败。报错:
ContractVerifierTest.java:[3,19] 找不到符号
[ERROR] 符号: 类 BaseTestClass
[ERROR] 位置: 程序包 toni.com.cn
这是因为,contract 插件默认会创建一个ContractVerifierTest类,而这个类的基础来源于POM.XML文件中插件配置的“<baseClassForTests>toni.com.cn.BaseTestClass</baseClassForTests>”这类,我没有去编辑也不需要编辑,因为我们当前,只是需要一个服务消费者一侧用到的stub,就不需要专门编制这样的一个类,通过忽略测试错误就可以略过此步骤依赖。事实上,自己编辑一个这个类是可以促成通过该检查的,只是那是在服务提供者一侧做mock进行Verifier验证测试的时候有意义,在本文的目的中不依赖此种类存在。
然后,就可以开发服务消费者的内部处理及有关的测试类、测试方法了。
本例子中服务消费者工程的基本信息是:
<groupId>toni.com.cn</groupId>
<artifactId>contract-sample-client</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>contract-sample-client</name>
<description>Demo project for Spring Boot</description>
服务消费者一侧的业务逻辑是:假设只是直白的把已有用户的id号和贷款金额提交给服务提供者(至于取得欺诈与否的判断后做什么事情在本实例中忽略掉,避免我们的关注焦点在过多其他业务细节中丢掉。)
创建客户类
package toni.com.cn.contract_sample_Client;
import lombok.Data;
@Data
public class Client {
private String id;
public Client(String id) {
this.id=id;
}
}
创建请求对象
package toni.com.cn.contract_sample_Client;
public class LoanRequire {
public String client_id;
public int loanAmount;
public LoanRequire(Client client,int loanAmount) {
this.client_id=client.getId();
this.loanAmount = loanAmount;
}
}
创建响应对象
package toni.com.cn.contract_sample_Client;
import lombok.Data;
@Data
public class LoanCheckResult {
private String checkStuats;
private String reason;
}
创建服务处理
package toni.com.cn.contract_sample_Client;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
@Service
public class ServiceForLoan {
@Autowired
private RestTemplate restTemplate;
//@Value(${ServerAdress})
private String ServerAdress="localhost";//这个地方建议参数ServerAdress配置化,这样避免开发测试环境和生产,
public LoanCheckResult loanCheck(LoanRequire request) throws RestClientException {
// TODO Auto-generated method stub
LoanCheckResult result=new LoanCheckResult();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
ResponseEntity<String> response = restTemplate.exchange("http://"+ServerAdress+":8090/fraudcheck",
HttpMethod.PUT,
new HttpEntity<>(request, headers), String.class);
if(response.getStatusCode()==HttpStatus.OK)
{
try {
ObjectMapper objectMapper = new ObjectMapper();
result = objectMapper.readValue(response.getBody(), LoanCheckResult.class);
} catch (JsonProcessingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return result;
}
}
说明:上面的代码中访问端口是9080的,其实也应该参数化,这里懒得写有关代码和配置了所以写死了。另外,处于contract测试的需要,这里访问的url是指向本地的,这里可以参数化,如何根据环境或者版本目标去动态的改变此参数是另外一个话题了,本文不详细描述。
以上实质上,都是业务领域编码的范畴,实际上不管有没有contract测试都是要写的。下面是contract测试类(在src/test/java的子目录中)和测试方法内容
package toni.com.cn.contract_sample_Client;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner;
import org.springframework.cloud.contract.stubrunner.spring.StubRunnerProperties.StubsMode;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
//@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = WebEnvironment.NONE)
@AutoConfigureStubRunner(ids = {"toni.com.cn:contract-sample-server:0.0.1-SNAPSHOT:stubs:8090"}, stubsMode = StubsMode.LOCAL)
public class LoanApplicationServiceTests {
@Autowired
ServiceForLoan service;
@Test
public void shouldBeRejectedDueToAbnormalLoanAmount() {
// given:
LoanRequire request = new LoanRequire(new Client("1234567890"),
99999);
// when:
LoanCheckResult loanresult = service.loanCheck(request);
// then:
assertThat(loanresult);
assertThat(loanresult.getCheckStuats())
.isEqualTo("FRAUD");
assertThat(loanresult.getReason()).isEqualTo("Amount too high");
System.out.println("看到这里,说明测试通过了");
}
}
源码中可以看到 此测试类需要被如下注解修饰
@AutoConfigureStubRunner(ids = {"toni.com.cn:contract-sample-server:0.0.1-SNAPSHOT:stubs:8090"}, stubsMode = StubsMode.LOCAL)
表明本测试类,依赖于contract的runner实现(POM.XML文件中需要加入有关dependency)指定前面文章中所创建的contract-sample-server-0.0.1-SNAPSHOT-stubs.jar来落地,通过ids就是有关的group-id:artifact-id:version:stub classifier :port 的组合来指定,这里我们设定为前面代码写死的8090向对应保持一致,同时强调了stubsMode 是本地的。
这里就值得和前面提到的有关机制呼应,介绍一下了,stubsMode是本地的,表明test时不需要像其他pom中的依赖项一样的到maven的公共库去取contract-sample-server-0.0.1-SNAPSHOT-stubs.jar包,而是在本地的maven库中取得。显然,这也要求,测试时服务消费者依赖的maven配置settings.xml和之前创建桩时所使用的是同样的配置,以及是同一个电脑上。因为其他文章,包括官网文章中都没有提到过这个细节,笔者在具体实践中,到此处时一直无法通过。报错类似如下:
“java.lang.IllegalStateException: Exception occurred while trying to download a stub for group [toni.com.cn] module [contract-sample-server] and classifier [stubs] in []”。原理而言,就是这个原因。
测试类开发完毕后,需要在服务消费者工程中,加入contract的依赖配置。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
<scope>test</scope>
</dependency>
注意:不需要加入“spring-cloud-starter-contract-verifier”依赖,加了也没有关系,不过并不必须。
加入cloud配置依赖
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud-release.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
cloud的版本配置,依然是:
<spring-cloud.version>2021.0.3</spring-cloud.version>
到这里,就可以进行测试验证了
通过eclipse选择LoanApplicationServiceTests进行测试了。此时直接执行测试,仍然会失败,报错:java.lang.IllegalStateException: Exception occurred while trying to download a stub for group [toni.com.cn] module [contract-sample-server] and classifier [stubs] in []
这是因为elipse在执行测试的时候,其使用的默认maven配置不能完全被contract测试类正确读取。导致根本就找不到本地库位置,所以就自然找不到目标 contract-sample-server-0.0.1-SNAPSHOT-stubs.jar。此时,在测试配置中修改一下 enviorement 环境参数,增加一个参数 org.apache.maven.user-settings 指向 settings.xml文件即可解决问题。类似如下图:
执行测试后,可以看到如下结果:
... ...
2022-09-02 17:19:39.203 INFO 16824 --- [ main] t.c.c.c.LoanApplicationServiceTests : Starting LoanApplicationServiceTests using Java 1.8.0_191 on DESKTOP-95SBFOT with PID 16824 (started by longlongago in D:\workspace\normal\contract-sample-client)
2022-09-02 17:19:39.205 INFO 16824 --- [ main] t.c.c.c.LoanApplicationServiceTests : No active profile set, falling back to 1 default profile: "default"
2022-09-02 17:19:41.268 INFO 16824 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.cloud.contract.stubrunner.spring.StubRunnerConfiguration' of type [org.springframework.cloud.contract.stubrunner.spring.StubRunnerConfiguration] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2022-09-02 17:19:41.299 INFO 16824 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'stubrunner-org.springframework.cloud.contract.stubrunner.spring.StubRunnerProperties' of type [org.springframework.cloud.contract.stubrunner.spring.StubRunnerProperties] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2022-09-02 17:19:41.318 INFO 16824 --- [ main] o.s.c.c.s.AetherStubDownloaderBuilder : Will download stubs and contracts via Aether
2022-09-02 17:19:41.400 INFO 16824 --- [ main] o.s.c.c.stubrunner.AetherStubDownloader : Remote repos not passed but the switch to work offline was set. Stubs will be used from your local Maven repository.
2022-09-02 17:19:41.732 INFO 16824 --- [ main] o.s.c.c.stubrunner.AetherStubDownloader : Resolved version is [0.0.1-SNAPSHOT]
2022-09-02 17:19:41.763 INFO 16824 --- [ main] o.s.c.c.stubrunner.AetherStubDownloader : Resolved artifact [toni.com.cn:contract-sample-server:jar:stubs:0.0.1-SNAPSHOT] to D:\mavenrepo\repository\toni\com\cn\contract-sample-server\0.0.1-SNAPSHOT\contract-sample-server-0.0.1-SNAPSHOT-stubs.jar
2022-09-02 17:19:41.765 INFO 16824 --- [ main] o.s.c.c.stubrunner.AetherStubDownloader : Unpacking stub from JAR [URI: file:/D:/mavenrepo/repository/toni/com/cn/contract-sample-server/0.0.1-SNAPSHOT/contract-sample-server-0.0.1-SNAPSHOT-stubs.jar]
2022-09-02 17:19:41.786 INFO 16824 --- [ main] o.s.c.c.stubrunner.AetherStubDownloader : Unpacked file to [C:\Users\LONGLO~1\AppData\Local\Temp\contracts-1662110381764-0]
... ...
2022-09-02 17:19:51.026 INFO 16824 --- [ qtp8039120-25] w.o.e.j.s.h.ContextHandler.__admin : RequestHandlerClass from context returned com.github.tomakehurst.wiremock.http.AdminRequestHandler. Normalized mapped under returned 'null'
2022-09-02 17:19:51.082 INFO 16824 --- [ qtp8039120-25] WireMock : Admin request received:
127.0.0.1 - POST /mappings
... ...
2022-09-02 17:19:51.262 INFO 16824 --- [ qtp8039120-24] WireMock : Admin request received:
127.0.0.1 - POST /mappings
... ...
2022-09-02 17:19:56.124 INFO 16824 --- [ qtp8039120-25] WireMock : Request received:
127.0.0.1 - PUT /fraudcheck
Accept: [text/plain, application/json, application/*+json, */*]
Content-Type: [application/json]
User-Agent: [Java/1.8.0_191]
Host: [localhost:8090]
Connection: [keep-alive]
Content-Length: [45]
{"client_id":"1234567890","loanAmount":99999}
Matched response definition:
{
"status" : 200,
"body" : "{\"checkStuats\":\"FRAUD\",\"reason\":\"Amount too high\"}",
"headers" : {
"Content-Type" : "application/json"
},
"transformers" : [ "response-template", "spring-cloud-contract" ]
}
Response:
HTTP/1.1 200
Content-Type: [application/json]
Matched-Stub-Id: [b78c3d70-98b5-45e4-8d0f-9f8837d1760e]
看到这里,说明测试通过了
2022-09-02 17:19:56.911 WARN 16824 --- [ main] .StubRunnerWireMockTestExecutionListener : You've used fixed ports for WireMock setup - will mark context as dirty. Please use random ports, as much as possible. Your tests will be faster and more reliable and this warning will go away
2022-09-02 17:19:56.942 INFO 16824 --- [ main] w.o.e.jetty.server.AbstractConnector : Stopped NetworkTrafficServerConnector@7f977fba{HTTP/1.1, (http/1.1, h2c)}{0.0.0.0:8090}
2022-09-02 17:19:56.945 INFO 16824 --- [ main] w.o.e.j.server.handler.ContextHandler : Stopped w.o.e.j.s.ServletContextHandler@34045582{/,null,STOPPED}
2022-09-02 17:19:56.946 INFO 16824 --- [ main] w.o.e.j.server.handler.ContextHandler : Stopped w.o.e.j.s.ServletContextHandler@41def031{/__admin,null,STOPPED}
就表明测试通过了。
实际上如果修改请求中的参数,会发现测试不通过,提示参数与预期不一致。这就起到了验证服务请求是否符合接口规格的作用。
合理的调整测试方法和断言,就可以验证服务消费者自身的处理是否符合预期了。这就提供了不依赖于服务提供者必须提供服务的实现和环境就可以进行服务消费者一侧自身的开发和验证条件。这就是测试驱动开发(TDD)了原理了。这样做的好处,就是在契约约定的基础条件下,服务消费者和服务提供者双方可以并行的各自开展开发工作,并且不用担心大家开发的接口假设完全不一致了。当然,这也说明,如果接口发生变化了,一定要通知stub的开发者修订接口脚本,重新发布stubs程序。
另外,由于contract是写死的请求和响应,也就是服务的响应是幂等的,因此在以后编码中,自动测试可以查看今后的代码修改中,是否对原来已经完成的功能产生了影响从而导致在同样的响应下却产生了和预期不一致的行为,这样就可以尽早发现修改带来的衍生BUG了,也就是自动进行回归测试了。
然后,为了让服务提供者一侧保持同步,特别是考虑到服务提供者也应该依赖此contract进行测试(verifier)和开发,因而,就需要把服务提供者这边为了生成契约而进行的改动pull提交给服务提供者团队。服务提供者团队在根据自身的情况和需要,进行版本合并。并进行后续的开发和自测。这一步而言,实际上在stub开发完毕的时候就可以提交了,不必须在服务消费者自身的开发基础上进行,不过应想办法确保stub的开发是正确、准确的(因为在生成stub的时候,我们有意选择了忽略测试错误)。
最后,我认为,关于stub的开发,前面提到不一定非要具备服务提供者版本的权限。是因为,实际上stubs.jar包是一个独立于服务端jar包的存在。解压contract-sample-server-0.0.1-SNAPSHOT-stubs.jar包,我们就可以发现,其实该包中根本没有任何的class文件,只有META-INF目录下的contract脚本文件和自动生成的映射文件。本质上就是一个很纯粹的挡板而已,所以理论上,完全可以不依赖服务提供者的程序而独立存在的。但是为了保持测试和测试依赖目标的一致性,至少还是须要保持artifactId的一致,也就是可以新创建一个空程序,POM.XML文件中设置和服务提供者工程主体一致即可.片段如下:
<groupId>toni.com.cn</groupId>
<artifactId>contract-sample-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>contract-sample-server</name>
这样的话,就不是必须下载服务提供者的工程就可以开发contract stub契约了。当然,这只是我的设想,并没有真正验证过。