笔者的经验认为,微服务的出现,是为了应对传统SOA架构在多服务背景下的疲软,本质上是SOA的进一步衍生。是一种治理服务的手段。而微服务之所以能够解决传统SOA、单块大单体程序的问题,原因在于微服务自身的健壮性、灵活性、可扩展性和持续敏捷。这些特性,在每一个工程、项目中,不管是业务领域模型而言,还是技术实现而言,都是通过持续集成、持续交付来落实的,或者说是通过反复而持续的试错、去除糟粕保留精华而持续精进的成果。

持续集成、持续交付的逻辑前提是:在为应对变化(主动也罢、被动也罢)所开展的持续修改、完善中,过程是尽量的顺滑的,低摩擦、低返工的,避免各种各样的问题导致卡停。这种情况主要源于软件开发的老短板,也就是修改导致故障的自然现象,所谓牵一发动全身的一般现实。新的功能需要快速被验证而不是等待业务测试人员手工的一环又一环的测试验证。已经改过的代码,很有可能导致原有的功能或服务失效、错误。解决问题是否有效,是否导致其他问题产生,需要明确、清晰而不是靠运气。有问题到发现,早解决,将极大的减少资源浪费,降低无止尽的返工导致的时间成本和工作量。这些,要用定点测试结合回归测试来落实。而人工端到端测试在效率上是灾难,不可能低成本做到回归测试。因此,只有自动测试才可能支撑真正有效果、有效率的持续集成、持续交付。

做项目也罢,开发软件也罢,都是有问题和缺陷的,尽早发现,尽早处理,这就是自动测试的价值和意义,也决定了自动测试实际上是持续集成、持续交付、有质量的微服务的重要内容和基石。在我以前的很多项目中,往往一到后期,就是各种BUG在SIT\UAT阶段纠缠,这是因为前面阶段部分甚至完全放弃了开发人员的自测而将所谓希望放在后续测试人员的手工测试上(效率很低、扯皮严重)。这种项目的痛苦,只有靠心大来应对,在漫不经心中无所谓或是持续扯皮、团队崩塌,这不该是技术工作可以有和应该有的样子。正因如此,在读到《敏捷革命》一书时,对杰夫·萨瑟兰所说的:想要找到方法以“指导团队变得更高效、更愉快、更具有相互扶持精神、更有乐趣以及更加令人向往”,对这样高尚同时也是睿智、务实的动机,我深以为然。达成这个目的,所依赖的重要方法正是持续集成、持续交付,他们的基石和新的梁柱手段就是自动测试,为了“更开心、更越快的工作”。

事实上,不管是《领域驱动设计》、还是《微服务架构设计模式》、《敏捷革命》等书以及马丁·福勒都一直在强调自动测试,这不是巧合,而是因为自动测试确实是当下软件行业进化的一个重要手段。通过自动测试才可能实现可信的小步快跑,使得机会、资源和精力合理匹配。

事实上,很早就已经有各种框架在支持自动测试,对于java语言而言就是junit,对于微服务而言比较主流的就是spring cloud contract。

spring cloud contract 是spring cloud的组件,不过完全可以与eruake服务发现这类核心组件分开看待,单独作为面向服务(如restful形式的服务)的集成测试框架来使用。事实上单元测试而言,不需要contract支持。contract的存在意义和价值在于集成测试和组件测试(验收测试一般在需要最接近真实生产的完整链条背景下,做端到端测试,这个环节反倒是维持传统的人员手工测试更合理些)。事实上,contract也是有效实现所谓测试驱动开发(TDD)的一种框架。contract的本质就是为服务的消费者(我喜欢称之为服务请求者、客户端client)提供后台服务的模拟(stub),也可以为服务的提供者(服务端 server)提供前端请求的模拟(mock)。后台服务的模拟(stub),是的服务消费者可以踏实的集中精力开发自身的业务逻辑(或者说领域模型),并测试是否正确的调用了后台服务(因为服务和消费者双方都要遵守契约)。响应的,请求的模拟(mock)可以是的服务提供者可以集中精力开发自身的领域模型,并验证提供的服务是否符合契约中的承诺,好修订错误。

contract的中文含义是契约,这就是经常会听到的“契约编程”的一种实质落地,也就是服务提供者和服务消费者,坐下来谈判和讨论,根据消费者的需求、提供者的条件和承诺(也许因为提供者太忙,就是懒得管消费者的需求,只给用现有接口,最多给你的接口定义文档和调试支持,爱用不用。也就是DDD中所谓的conformist-跟随者模式,所以这个谈判是必然的,或者说有条件的话最好谈判清楚)形成服务的协议,其实就是接口的定义(一般表现为:一个请求和响应的例子,包括通信方式、消息格式、消息组成及其意思和确切的内容)。在contract框架中,这个接口需要由双方协商确定后,编写一个定义文件(我喜欢叫做contract脚本文件)。一般是grovvy语言的文件,也可以是yaml的。关于该文件是由服务提供者还是服务请求者编辑?其实都可以,看合作模式和文化了。有关细节在后续的内容中,我再详细描述。基于这个contract脚本文件,contract框架可以生成一个stubs(桩),这个桩的目的在于给服务请求者提供后台服务接口的模拟(stub)条件。如何生成,以及如何让这个桩工作起来,后续内容我在详细描述。有了这个桩(stub),服务请求者就可以在test类中编辑测试方法中 通过模拟调用遵守contract定义的接口的内部服务A,从而既验证调用的方式是否正确,又基于模拟响应的内容从而对内部服务A的实现进行验证(比如对响应回来的数据进行在加工以后的结果是否符合预期),基于测试方法中的断言,程序可以在编译打包时会提示有关的实现是否正确,从而能够在此阶段就发现和修正错误,避免BUG沉淀到后面(如真正的集成阶段,甚至生产运行阶段)才暴露出来。(另:由于篇幅和复杂度的控制,本章只讲述spring contract对于服务消费者一侧的意义和使用,服务提供者一侧在以后的文章中提供)

下面是我所理解的过程序列和概念示意图来表达这个过程的逻辑。

事实上,在以前我自己利用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 contractsorg.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型王者,没有权限问题。这是有问题的,因为微服务的本质是把复杂度相互隔离开,把复杂分解下来,这样就可以把权利和责任分派出去。如果没有彼此,耦合度必然会有意无意的越来越紧,混淆在一起。当然,也有可能,服务消费者团队根本不具备服务提供者团队工程的代码权限,其实也可以做到对上面创建的模拟stub桩的使用,这我在后面会提到。

然后,在该工程下,执行 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包。这个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;
}

创建 服务消费者自身的一个业务规则处理 (也使用了service关键字,是因为这个对象是为本服务消费者工程中的客户service的,可能容易于被模拟的stub混淆)。在这个业务规则处理类 中,将调用被模拟的服务的桩(stub),实际上这个stub 已经通过配置POM.XML,纳入到本工程的依赖库中。在执行test测试时,将会和本工程一起启动,并占用8090端口,该端口是多少可以在测试类中设定和修改(后面可以看到),不过最好保持与被模拟的服务一致,从而尽量保持测试时候的代码、配置和真实运行时一致。也可以联想得到,由于在同一个设备上运行,如果本服务消费者也是一个提供HTTP服务的程序,那要注意需要本工程配置成非8090端口的监听才能启动得起来(避免端口争抢得情况)。

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 {@Autowiredprivate RestTemplate restTemplate;//@Value(${ServerAdress})private String ServerAdress="localhost";//这个地方建议参数ServerAdress配置化,这样避免开发测试环境和生产,public LoanCheckResult loanCheck(LoanRequire request) throws RestClientException {// TODO Auto-generated method stubLoanCheckResult 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 blocke.printStackTrace();}}return result;}
}

说明:出于contract测试的需要,这里访问的url是指向本地的,这里可以参数化,如何根据环境或者版本目标去动态的改变此参数是另外一个话题了,比如完全可以把stubs.jar工程放在另一个设备上,启动起来,本文不详细描述。

以上实质上,都是业务领域编码的范畴,实际上不管有没有contract测试都是要写的。正是如此,我们也可以看到,测试、测试依赖的模拟(stub)和真正需要的工作代码、领域代码是完全分开的,工作代码根本不知道、不需要知道测试桩、测试类的存在。

下面是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 {@AutowiredServiceForLoan service;@Testpublic 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 /fraudcheckAccept: [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)和开发。因而,如果服务消费者团队对contract脚本等进行了改动,应及时pull提交到远程的maven库中。服务提供者团队在根据自身的情况和需要,进行版本合并。并进行后续的开发和自测。这一步而言,实际上在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契约了。当然,这只是我的设想,并没有真正验证过。

spring cloud contract的应用实现与概念理解-服务请求者一侧的落地-细节较多避免踩坑卡壳相关推荐

  1. spring cloud contract的应用实现与概念理解-服务提供者一侧的落地

    如题,本文是在前一篇"spring cloud contract的应用实现与概念理解-服务请求者一侧的落地"的基础上,续写服务提供者一侧的有关实现与理解. 通过对官网文章的学习和编 ...

  2. 消费者驱动的契约测试 Spring Cloud Contract介绍

    消费者驱动的契约测试 Spring Cloud Contract介绍 什么是契约测试 测试是软件流程中非常重要,不可或缺的一个环节.一般的测试分为单元测试,集成测试,端到端的手工测试,这也是构成测试金 ...

  3. 契约测试之Spring Cloud Contract

    在微服务架构下,服务间会通过某种形式的消息传递或API调用进行耦合,这让服务的集成以及测试变成了非常具有挑战的一件事.早在微服务流行之前,就有人提出了消费者驱动契约(Consumer-driven c ...

  4. 消费者驱动的微服务契约测试套件:Spring Cloud Contract

    在微服务架构下,你的服务可能由不同的团队提供和维护,在这种情况下,接口的开发和维护可能会带来一些问题,比如服务端调整架构或接口调整而对消费者不透明,导致接口调用失败. 为解决这些问题,Ian Robi ...

  5. 消费者驱动的微服务契约测试套件Spring Cloud Contract

    在微服务架构下,你的服务可能由不同的团队提供和维护,在这种情况下,接口的开发和维护可能会带来一些问题,比如服务端调整架构或接口调整而对消费者不透明,导致接口调用失败. 为解决这些问题,Ian Robi ...

  6. Marcin Grzejszczak访谈:Spring Cloud Contract

    Marcin Grzejszczak是Pivotal的一名软件工程师.目前,他在从事Spring Cloud Contract的开发,这是一个消费者驱动的.面向Java的契约框架.为了了解该框架的一些 ...

  7. Spring Cloud Contract 契约测试实践

    本文转载公众号:永辉云创技术 该号由我参与维护,欢迎大家关注支持!!! 分布式研发模型演进 众所周知, 分布式系统是由众多微服务构成,并按照功能模块划分后, 由不同的开发小组进行维护. 研发模型如下图 ...

  8. Spring Cloud Contract实践

    1.Spring Cloud Contract简介 Spring Cloud Contract是一个总体项目,其中包含帮助用户成功实施消费者驱动合同方法的解决方案.目前,Spring Cloud Co ...

  9. Spring Cloud Contract 初识之一 :简介

    1.简介 Spring Cloud Contract 是一个包含解决方案的综合项目,帮助用户成功地向 Consumer Driven Contracts 进行处理.目前,Spring Cloud Co ...

最新文章

  1. 分享一个expect的脚本
  2. Erlang--热更新
  3. [转]Java8-本地缓存
  4. uitextfield 键盘类型_UITextField 键盘弹出问题
  5. AT5661-[AGC040C]Neither AB nor BA【模型转换】
  6. P1446-[HNOI2008]Cards【Burnside引理,dp】
  7. 面试大厂应该注意哪些问题?算法太TM重要了
  8. Java 中创建对象方式
  9. csm和uefi_关于CSM和UEFI你要知道的一些事
  10. jvm 解释器和编译器
  11. 十、工业相机与SCARA机械臂的坐标系标定
  12. vue绑定背景图片有括号无法显示怎么办
  13. 关于Unable to read additional data from server sessionid 0x0问题的解决
  14. 计算机应用 一级学科,一级学科、二级学科,考研专业哪个包含了计算机考研方向...
  15. wx-jssdk,IOS调用API,config通过了,但是报错[ the permission value is offline verifying ]
  16. python爬虫的线程、进程、异步的基础讲解
  17. 使用jib-maven-plugin分层构建Docker镜像——避免直接使用FatJar
  18. 利用java对接阿里云sls服务(aliyun-log)做浏览记录
  19. 架设游戏私服——内网穿透工具frp
  20. weblogic12c重置密码linux,weblogic 12c忘记domain密码重置方法

热门文章

  1. Excel 怎样去掉单元格中的回车符号
  2. 经典激光雷达SLAM系统:LOAM-Livox
  3. Gstreamer/tools: gst-launch gst-inspect 用法示例
  4. 【Python】时间序列分析
  5. 有介质的高斯定理详细证明(电偶极子模型)以及例题讲解
  6. video视频关键帧截取
  7. 弱口令暴力破解详解(包含工具、字典下载地址)
  8. 点击按钮显示文字,再次点击隐藏文字
  9. oracle中的cursor属性,Oracle Cursors语法总结
  10. (实测)Discuz修改论坛最后发表的帖子的链接为静态地址