如题,本文是在前一篇“spring cloud contract的应用实现与概念理解-服务请求者一侧的落地”的基础上,续写服务提供者一侧的有关实现与理解。
通过对官网文章的学习和编程实践的验证,contract的应用实现,实际上并不须要在同一个工程中同时完成stub桩(给服务消费者用)和mock模拟(给服务提供者用)。经常看到的例子中,既包含“spring-cloud-starter-contract-verifier"又包含“spring-cloud-starter-contract-stub-runner”(POM.XML文件中配置依赖),那也许是因为微服务中,很多工程实际上既是另外某个服务消费者,也是其他服务或者客户端的服务提供者。如果把问题分解,单独从服务提供者的角度而言,实际上更简单。
集成测试的背景、意义,contract在集成测试中的位置和作用,在我的前一篇文章spring cloud contract的应用实现与概念理解-服务请求者一侧的落地-细节较多避免踩坑卡壳已经进行了比较感性的描述,本文就不再累述。下面就直接从服务提供者一侧开发的目的、过程和内容来记述。首先,服务提供者利用contract的目的抛开给消费者提供确定的接口规格桩外,对于服务提供者自身,是形成一个测试发起者的模拟(mock),向协议脚本中定义的服务URL(REST API如此,消息队列是另外的参数,不在本文中深究)提交预期定好的请求,服务提供者此时的服务实现应该已经完成开发并能响应,contract将会在测试方法中对响应进行比对验证,所有要素的符合预期的话,则达到了验证服务有效性、正确性的目的。
服务提供者一侧contract的编辑和实现,实际上很简单。具体步骤其实,在前一篇文章中已经提到了。此时,首先由服务消费者和服务提供者设计讨论服务的接口规格,然后由服务提供者在版本中POM.XML增加contract的插件配置,再针对要测试的服务接口编辑contract脚本文件就可以了。就这么简单。过程如下图:后面将会解析其背后的逻辑。
过程和内容而言,很简单。
首先,在POM中增加(假设第一次在这个工程中开发contract测试)
首先,增加spring cloud的版本定义,因为contract实际上是spring 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>
其中,spring cloud 和 contract的版本配置如下(2022年9月 经过实践验证,可用)
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring-cloud-release.version>2021.0.3</spring-cloud-release.version>
<spring-cloud-contract.version>3.1.3</spring-cloud-contract.version>
</properties>
然后,加入contract构建插件,这个插件的作用在于依据contract脚本生成stub桩。
在build--plugins下增加
<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.contract_sample.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>
设定此插件的同时,按照官网的说法,服务提供者一侧的有关依赖就也包含在里面了。不过,我在具体案例中,还是加入了verifier依赖,如下:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-verifier</artifactId>
<scope>test</scope>
</dependency>
值得专门说明的地方,在“spring-cloud-contract-maven-plugin”中,有一行<baseClassForTests>toni.com.cn.contract_sample.BaseTestClass</baseClassForTests>目的在于告知contract“引擎”将该BaseTestClass类作为所有contract测试类的基类。后续的编码中,由于服务提供者需要对自己的服务进行测试,所以是需要开发人员编写这个类(BaseTestClass),但实际上不需要在该类中实现测试方法。只需要进行一个测试服务的加载即可(本案例中服务入口的controller是FraudController),至于为什么不需要测试方法,后面会介绍。代码如下:
package toni.com.cn.contract_sample;
import toni.com.cn.contract_sample.controller.FraudController;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.test.context.SpringBootTest;
import io.restassured.module.mockmvc.RestAssuredMockMvc;
import lombok.extern.slf4j.Slf4j;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = toni.com.cn.contract_sample.App.class)
@Slf4j
public class BaseTestClass {
@BeforeEach
public void setup() {
RestAssuredMockMvc.standaloneSetup(new FraudController());
}
}
接下来,就是依据协商好的接口编制contract脚本,脚本的位置应该在src\test\resources的contracts目录(自建)中,contract脚本名无所谓,可以自己取。比如“service_first_sample.groovy”
脚本内容如下:
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')
}
}
}
到此,测试框架和测试用例的开发就完成了。不过测试肯定通过不了,因为被测试的服务本身没有完成。
服务的代码后续会附上,这里先说明一下为什么不需要开发专门针对具体服务的测试类。这是因为contract框架在工程的install处理时,会自动前面定义的基类创建一个contract测试类,类名是ContractVerifierTest,这个类的JAVA文件是自动产生的,将会在target目录中,而不是开发者的src目录中。该类将依据contract脚本,自行生成对应的测试请求和响应对于的测试方法加入的该测试类中。并在工程install时自动执行测试。测试的内容就是依据契约模拟请求,向服务发送请求,并验证每一个响应元素。服务实现没有完成或者响应不符合预期都会断言失败,相应的如果没有断言失败则说明测试通过。比如本案例中,ContractVerifierTest将产生一个测试方法如下:
@Test
public void validate_service_first_sample() throws Exception {
// given:
MockMvcRequestSpecification request = given()
.header("Content-Type", "application/json")
.body("{\"client_id\":\"7375534648\",\"loanAmount\":99999}");
// when:
ResponseOptions response = given().spec(request)
.put("/fraudcheck");
// then:
assertThat(response.statusCode()).isEqualTo(200);
assertThat(response.header("Content-Type")).matches("application/json.*");
// and:
DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
assertThatJson(parsedJson).field("['checkStuats']").isEqualTo("FRAUD");
assertThatJson(parsedJson).field("['reason']").isEqualTo("Amount too high");
}
从方法名到方法内容,都是contract框架自动产生的。所以,就服务而言,开发者根本不需要额外编辑测试方法和测试类。如果想自己开发其他更复杂的测试,也可以。
从服务提供者的角度而言,针对想测试的服务,将相应的contract脚本文件编辑好就可以了,都会产生响应的测试方法到该ContractVerifierTest类中或者其基类中,不需要额外编辑其他测试类。就是这么简单。
这样的话,服务提供者就不需要服务请求者开发好来在实际集成环境中来测试,而是自己就可以进行测试了。
附:本案例服务的代码如下:
controller类
package toni.com.cn.contract_sample.controller;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import lombok.extern.slf4j.Slf4j;
import toni.com.cn.contract_sample.entity.LoanRequest;
@RestController
@Slf4j
public class FraudController {
@PutMapping(value = "/fraudcheck", consumes="application/json", produces="application/json")
public String check(@RequestBody LoanRequest loanRequest) {
log.info("请求的参数是:{}",loanRequest);
if (loanRequest.getLoanAmount() > 10000) {
return "[{checkStuats: FRAUD, reason: Amount too high}]";
} else {
return "[{checkStuats: OK, reason: Amount OK}]";
}
}
}
请求对象:
package toni.com.cn.contract_sample.entity;
import com.fasterxml.jackson.annotation.JsonProperty;
public class LoanRequest {
@JsonProperty("client.id")
private String clientId;
private Long loanAmount;
public String getClientId() {
return clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public Long getLoanAmount() {
return loanAmount;
}
public void setLoanRequestAmount(Long loanAmount) {
this.loanAmount = loanAmount;
}
}