开发你的第一个基于 Spring Cloud Contract 的应用
本简短教程将引导您使用 Spring Cloud Contract。它包含以下主题:
你可以在这里找到更简短的介绍:三秒快速入门。
在本示例中,Stub Storage 为 Nexus/Artifactory。
下面的UML图展示了Spring Cloud Contract各组件之间的关系:

生产者端
要开始使用 Spring Cloud Contract,你可以将 Spring Cloud Contract Verifier 的依赖项和插件添加到构建文件中,如下例所示:
下面的代码展示了如何添加插件,该插件应放置在文件的 build/plugins 部分:
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>${spring-cloud-contract.version}</version>
<extensions>true</extensions>
</plugin>
开始使用的最简单方法是访问 Spring Initializr 并添加 "Web" 和 "Contract Verifier" 作为依赖项。这样做会引入前面提到的依赖项以及 pom.xml 文件中所需的所有其他内容(除了设置基础测试类,我们将在本节后面介绍)。下图显示了在 Spring Initializr 中应使用的设置:

现在,你可以将使用 Groovy DSL 或 YAML 表达的 REST/ 消息契约文件添加到 contracts 目录中,该目录由 contractsDslDir 属性设置。默认情况下,它是 $rootDir/src/test/resources/contracts。请注意,文件名并不重要。你可以在此目录中按照自己喜欢的命名方案来组织你的契约文件。
对于HTTP存根,契约定义了针对特定请求应返回何种响应(需考虑HTTP方法、URL、头部信息、状态码等)。以下示例展示了Groovy和YAML两种格式的HTTP存根契约:
groovy
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([
fraudCheckStatus: "FRAUD",
"rejection.reason": "Amount too high"
])
headers {
contentType('application/json')
}
}
}
yaml
request:
method: PUT
url: /fraudcheck
body:
"client.id": 1234567890
loanAmount: 99999
headers:
Content-Type: application/json
matchers:
body:
- path: $.['client.id']
type: by_regex
value: "[0-9]{10}"
response:
status: 200
body:
fraudCheckStatus: "FRAUD"
"rejection.reason": "Amount too high"
headers:
Content-Type: application/json;charset=UTF-8
如果你需要使用消息传递,可以定义:
-
输入和输出消息(需考虑发送来源、消息正文及消息头)。
-
接收到消息后应调用的方法。
-
调用时应触发消息的方法。
以下示例展示了一个Camel消息传递契约:
groovy
def contractDsl = Contract.make {
name "foo"
label 'some_label'
input {
triggeredBy('bookReturnedTriggered()')
}
outputMessage {
sentTo('activemq:output')
body('''{ "bookName" : "foo" }''')
headers {
header('BOOK-NAME', 'foo')
messagingContentType(applicationJson())
}
}
}
yaml
label: some_label
input:
triggeredBy: bookReturnedTriggered
outputMessage:
sentTo: activemq:output
body:
bookName: foo
headers:
BOOK-NAME: foo
contentType: application/json
运行 ./mvnw clean install 会自动生成测试,以验证应用程序是否符合添加的契约。默认情况下,生成的测试位于 org.springframework.cloud.contract.verifier.tests. 目录下。
生成的测试可能会有所不同,具体取决于您在插件中设置的框架和测试类型。
在接下来的列表中,你可以找到:
-
在
MockMvc中 HTTP 合约的默认测试模式 -
使用
JAXRS测试模式的 JAX-RS 客户端 -
基于
WebTestClient的测试(特别推荐用于基于响应式Web-Flux的应用程序),使用WEBTESTCLIENT测试模式进行设置
你只需要这些测试框架中的一个。MockMvc 是默认选项。要使用其他框架,请将其库添加到你的类路径中。
以下列表展示了所有框架的示例:
mockmvc
@Test
public void validate_shouldMarkClientAsFraud() throws Exception {
// given:
MockMvcRequestSpecification request = given()
.header("Content-Type", "application/vnd.fraud.v1+json")
.body("{\"client.id\":\"1234567890\",\"loanAmount\":99999}");
// when:
ResponseOptions response = given().spec(request)
.put("/fraudcheck");
// then:
assertThat(response.statusCode()).isEqualTo(200);
assertThat(response.header("Content-Type")).matches("application/vnd.fraud.v1.json.*");
// and:
DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
assertThatJson(parsedJson).field("['fraudCheckStatus']").matches("[A-Z]{5}");
assertThatJson(parsedJson).field("['rejection.reason']").isEqualTo("Amount too high");
}
jaxrs
public class FooTest {
WebTarget webTarget;
@Test
public void validate_() throws Exception {
// when:
Response response = webTarget
.path("/users")
.queryParam("limit", "10")
.queryParam("offset", "20")
.queryParam("filter", "email")
.queryParam("sort", "name")
.queryParam("search", "55")
.queryParam("age", "99")
.queryParam("name", "Denis.Stepanov")
.queryParam("email", "bob@email.com")
.request()
.build("GET")
.invoke();
String responseAsString = response.readEntity(String.class);
// then:
assertThat(response.getStatus()).isEqualTo(200);
// and:
DocumentContext parsedJson = JsonPath.parse(responseAsString);
assertThatJson(parsedJson).field("['property1']").isEqualTo("a");
}
}
webtestclient
@Test
public void validate_shouldRejectABeerIfTooYoung() throws Exception {
// given:
WebTestClientRequestSpecification request = given()
.header("Content-Type", "application/json")
.body("{\"age\":10}");
// when:
WebTestClientResponse response = given().spec(request)
.post("/check");
// 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("['status']").isEqualTo("NOT_OK");
}
由于合约所描述的功能尚未实现,测试会失败。
要让它们通过,你必须添加处理HTTP请求或消息的正确实现。同时,你必须在项目中为自动生成的测试添加一个基础测试类。这个类被所有自动生成的测试所继承,并且应该包含运行这些测试所需的所有必要设置信息(例如,RestAssuredMockMvc 控制器设置或消息测试设置)。
以下示例来自 pom.xml,展示了如何指定基础测试类:
<build>
<plugins>
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>2.1.2.RELEASE</version>
<extensions>true</extensions>
<configuration>
<baseClassForTests>com.example.contractTest.BaseTestClass</baseClassForTests> // <1>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
baseClassForTests元素允许您指定基础测试类。它必须是spring-cloud-contract-maven-plugin内configuration元素的子元素。
以下示例展示了一个最小化(但功能完整)的基础测试类:
package com.example.contractTest;
import org.junit.Before;
import io.restassured.module.mockmvc.RestAssuredMockMvc;
public class BaseTestClass {
@Before
public void setup() {
RestAssuredMockMvc.standaloneSetup(new FraudController());
}
}
这个最小化的类确实是让测试运行起来所需的全部内容。它作为一个起点,自动生成的测试会附加到它上面。
现在我们可以继续实现。为此,我们首先需要一个数据类,然后将其用于我们的控制器。以下清单展示了这个数据类:
package com.example.Test;
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;
}
}
前面的类提供了一个对象,我们可以用它来存储参数。由于合约中的客户端ID名为client.id,我们需要使用@JsonProperty("client.id")参数将其映射到clientId字段。
现在我们可以继续来看控制器,如下列表所示:
package com.example.docTest;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class FraudController {
@PutMapping(value = "/fraudcheck", consumes="application/json", produces="application/json")
public String check(@RequestBody LoanRequest loanRequest) { 1
if (loanRequest.getLoanAmount() > 10000) { 2
return "{fraudCheckStatus: FRAUD, rejection.reason: Amount too high}"; 3
} else {
return "{fraudCheckStatus: OK, acceptance.reason: Amount OK}"; 4
}
}
}
我们将传入的参数映射到一个
LoanRequest对象。我们检查请求的贷款金额是否过高。
如果金额过高,我们返回测试期望的 JSON(这里使用简单字符串创建)。
如果我们有一个测试来捕获允许的金额情况,我们可以将其与此输出匹配。
FraudController 的设计极其简洁。你可以在此基础上实现更多功能,包括日志记录、验证客户端ID等。
一旦实现和测试基类准备就绪,测试通过,应用程序和存根构件都会构建并安装到本地 Maven 仓库中。关于将存根 jar 安装到本地仓库的信息会出现在日志中,如下例所示:
[INFO] --- spring-cloud-contract-maven-plugin:1.0.0.BUILD-SNAPSHOT:generateStubs (default-generateStubs) @ http-server ---
[INFO] Building jar: /some/path/http-server/target/http-server-0.0.1-SNAPSHOT-stubs.jar
[INFO]
[INFO] --- maven-jar-plugin:2.6:jar (default-jar) @ http-server ---
[INFO] Building jar: /some/path/http-server/target/http-server-0.0.1-SNAPSHOT.jar
[INFO]
[INFO] --- spring-boot-maven-plugin:1.5.5.BUILD-SNAPSHOT:repackage (default) @ http-server ---
[INFO]
[INFO] --- maven-install-plugin:2.5.2:install (default-install) @ http-server ---
[INFO] Installing /some/path/http-server/target/http-server-0.0.1-SNAPSHOT.jar to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT.jar
[INFO] Installing /some/path/http-server/pom.xml to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT.pom
[INFO] Installing /some/path/http-server/target/http-server-0.0.1-SNAPSHOT-stubs.jar to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT-stubs.jar
现在,你可以合并更改,并在在线仓库中发布应用程序和存根构件。
在消费者端
你可以在集成测试中使用 Spring Cloud Contract Stub Runner 来获取一个运行中的 WireMock 实例或消息路由,以模拟实际服务。
要开始使用,请添加 Spring Cloud Contract Stub Runner 依赖,如下所示:
你可以通过以下两种方式之一将 Producer 端存根安装到你的 Maven 仓库中:
-
通过检出生产者端仓库,添加合约并通过运行以下命令生成存根:
$ cd local-http-server-repo
$ ./mvnw clean install -DskipTests备注跳过测试是因为生产者端的合约实现尚未就绪,因此自动生成的合约测试会失败。
-
通过从远程仓库获取现有的生产者服务存根。为此,将存根构件 ID 和构件仓库 URL 作为
Spring Cloud Contract Stub Runner属性传递,如下例所示:
现在你可以在测试类上标注 @AutoConfigureStubRunner 注解。在该注解中,为 Spring Cloud Contract Stub Runner 提供 group-id 和 artifact-id,以便它为你运行协作者的存根,如下例所示:
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment=WebEnvironment.NONE)
@AutoConfigureStubRunner(ids = {"com.example:http-server-dsl:+:stubs:6565"},
stubsMode = StubRunnerProperties.StubsMode.LOCAL)
public class LoanApplicationServiceTests {
. . .
}
从在线仓库下载存根时使用 REMOTE stubsMode,离线工作时使用 LOCAL。
在集成测试中,你可以接收到模拟版本的HTTP响应或预期由协作服务发出的消息。你可以在构建日志中看到类似以下条目:
2016-07-19 14:22:25.403 INFO 41050 --- [ main] o.s.c.c.stubrunner.AetherStubDownloader : Desired version is + - will try to resolve the latest version
2016-07-19 14:22:25.438 INFO 41050 --- [ main] o.s.c.c.stubrunner.AetherStubDownloader : Resolved version is 0.0.1-SNAPSHOT
2016-07-19 14:22:25.439 INFO 41050 --- [ main] o.s.c.c.stubrunner.AetherStubDownloader : Resolving artifact com.example:http-server:jar:stubs:0.0.1-SNAPSHOT using remote repositories []
2016-07-19 14:22:25.451 INFO 41050 --- [ main] o.s.c.c.stubrunner.AetherStubDownloader : Resolved artifact com.example:http-server:jar:stubs:0.0.1-SNAPSHOT to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT-stubs.jar
2016-07-19 14:22:25.465 INFO 41050 --- [ main] o.s.c.c.stubrunner.AetherStubDownloader : Unpacking stub from JAR [URI: file:/path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT-stubs.jar]
2016-07-19 14:22:25.475 INFO 41050 --- [ main] o.s.c.c.stubrunner.AetherStubDownloader : Unpacked file to [/var/folders/0p/xwq47sq106x1_g3dtv6qfm940000gq/T/contracts100276532569594265]
2016-07-19 14:22:27.737 INFO 41050 --- [ main] o.s.c.c.stubrunner.StubRunnerExecutor : All stubs are now running RunningStubs [namesAndPorts={com.example:http-server:0.0.1-SNAPSHOT:stubs=8080}]