开始
1. Spring Cloud Contract 简介
Spring Cloud Contract 将 TDD 移至软件体系结构级别。 它允许您执行使用者驱动和生产者驱动的合同测试。
1.1. 历史
在成为 Spring Cloud Contract 之前,这个项目被称为 Accurest。 它由来自 (Codearte) 的 Marcin Grzejszczak 和 Jakub Kubrynski 创建。
这0.1.0
发布于 2015 年 1 月 26 日,并随着1.0.0
于 2016 年 2 月 29 日发布。
1.1.2. 测试问题
如果我们想测试前面图片左上角的应用程序 部分来确定它是否可以与其他服务通信,我们可以执行 两件事:
-
部署所有微服务并执行端到端测试。
-
在单元测试和集成测试中模拟其他微服务。
两者都有其优点,但也有很多缺点。
部署所有微服务并执行端到端测试
优势:
-
模拟生产。
-
测试服务之间的真实通信。
弊:
-
要测试一个微服务,我们必须部署六个微服务、几个数据库、 和其他项目。
-
运行测试的环境被锁定为一组测试(没有其他人 将能够同时运行测试)。
-
它们需要很长时间才能运行。
-
反馈在这个过程中很晚才出现。
-
它们非常难以调试。
在单元测试和集成测试中模拟其他微服务
优势:
-
他们提供非常快速的反馈。
-
他们没有基础设施要求。
弊:
-
服务的实现者创建的存根可能与 现实。
-
您可以在通过测试但生产失败的情况下进入生产环境。
为了解决上述问题,创建了 Spring Cloud Contract。主要思想是 为您提供非常快速的反馈,而无需设置 整个微服务世界。如果您处理存根,那么您唯一需要的应用程序 是您的应用程序直接使用的值。下图显示了这种关系 of stub 添加到应用程序:

Spring Cloud Contract 可以确定您使用的存根是 由您调用的服务创建。此外,如果您可以使用它们,则意味着它们 对制片人一方进行了测试。简而言之,您可以信任这些存根。
1.2. 目的
Spring Cloud Contract 的主要目的是:
-
确保 HTTP 和消息传递存根(在开发客户端时使用)完全执行 实际的服务器端实现的作用。
-
推广 ATDD (验收测试驱动开发) 方法和微服务架构风格。
-
提供一种在合同中发布更改的方法,这些更改在双方都立即可见。
-
生成要在服务器端使用的样板测试代码。
默认情况下,Spring Cloud Contract 作为 HTTP 服务器存根与 Wiremock 集成。
Spring Cloud Contract 的目的不是开始编写业务 合同中的特征。假设我们有一个欺诈检查的业务使用案例。如果 用户可能由于 100 种不同的原因而成为欺诈者,我们假设您会创建两个 合同,一个用于 positive 情况,一个用于 negative 情况。Contract 测试包括 用于测试应用程序之间的协定,而不是模拟完整行为。 |
1.3. 什么是合同?
作为服务的消费者,我们需要定义我们到底想要实现什么。我们需要 制定我们的期望。这就是我们编写合同的原因。换句话说,合约是 关于 API 或消息通信外观的协议。请考虑以下示例:
假设您要发送一个请求,其中包含客户公司的 ID 和
它想向我们借款的金额。您还需要将其发送到/fraudcheck
URL 使用
这PUT
方法。下面的清单显示了一个合同,用于检查客户是否应该
在 Groovy 和 YAML 中都标记为欺诈:
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package contracts
org.springframework.cloud.contract.spec.Contract.make {
request { // (1)
method 'PUT' // (2)
url '/fraudcheck' // (3)
body([ // (4)
"client.id": $(regex('[0-9]{10}')),
loanAmount : 99999
])
headers { // (5)
contentType('application/json')
}
}
response { // (6)
status OK() // (7)
body([ // (8)
fraudCheckStatus : "FRAUD",
"rejection.reason": "Amount too high"
])
headers { // (9)
contentType('application/json')
}
}
}
/*
From the Consumer perspective, when shooting a request in the integration test:
(1) - If the consumer sends a request
(2) - With the "PUT" method
(3) - to the URL "/fraudcheck"
(4) - with the JSON body that
* has a field `client.id` that matches a regular expression `[0-9]{10}`
* has a field `loanAmount` that is equal to `99999`
(5) - with header `Content-Type` equal to `application/json`
(6) - then the response will be sent with
(7) - status equal `200`
(8) - and JSON body equal to
{ "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" }
(9) - with header `Content-Type` equal to `application/json`
From the Producer perspective, in the autogenerated producer-side test:
(1) - A request will be sent to the producer
(2) - With the "PUT" method
(3) - to the URL "/fraudcheck"
(4) - with the JSON body that
* has a field `client.id` that will have a generated value that matches a regular expression `[0-9]{10}`
* has a field `loanAmount` that is equal to `99999`
(5) - with header `Content-Type` equal to `application/json`
(6) - then the test will assert if the response has been sent with
(7) - status equal `200`
(8) - and JSON body equal to
{ "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" }
(9) - with header `Content-Type` matching `application/json.*`
*/
request: # (1)
method: PUT # (2)
url: /yamlfraudcheck # (3)
body: # (4)
"client.id": 1234567890
loanAmount: 99999
headers: # (5)
Content-Type: application/json
matchers:
body:
- path: $.['client.id'] # (6)
type: by_regex
value: "[0-9]{10}"
response: # (7)
status: 200 # (8)
body: # (9)
fraudCheckStatus: "FRAUD"
"rejection.reason": "Amount too high"
headers: # (10)
Content-Type: application/json
#From the Consumer perspective, when shooting a request in the integration test:
#
#(1) - If the consumer sends a request
#(2) - With the "PUT" method
#(3) - to the URL "/yamlfraudcheck"
#(4) - with the JSON body that
# * has a field `client.id`
# * has a field `loanAmount` that is equal to `99999`
#(5) - with header `Content-Type` equal to `application/json`
#(6) - and a `client.id` json entry matches the regular expression `[0-9]{10}`
#(7) - then the response will be sent with
#(8) - status equal `200`
#(9) - and JSON body equal to
# { "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" }
#(10) - with header `Content-Type` equal to `application/json`
#
#From the Producer perspective, in the autogenerated producer-side test:
#
#(1) - A request will be sent to the producer
#(2) - With the "PUT" method
#(3) - to the URL "/yamlfraudcheck"
#(4) - with the JSON body that
# * has a field `client.id` `1234567890`
# * has a field `loanAmount` that is equal to `99999`
#(5) - with header `Content-Type` equal to `application/json`
#(7) - then the test will assert if the response has been sent with
#(8) - status equal `200`
#(9) - and JSON body equal to
# { "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" }
#(10) - with header `Content-Type` equal to `application/json`
预计 Contract 来自受信任的来源。切勿下载来自不受信任位置的合约,也不应与之交互。 |
2. 三秒游览
这个非常简短的教程将介绍如何使用 Spring Cloud Contract。它由 以下主题:
你可以在这里找到一个稍长的旅行。
下面的 UML 图显示了 Spring Cloud Contract 中各部分的关系:

2.1. 在 Producer 端
要开始使用 Spring Cloud Contract,您可以使用 REST 或消息传递 Contract 添加文件
以 Groovy DSL 或 YAML 表示到 contracts 目录,该目录由contractsDslDir
财产。默认情况下,它是$rootDir/src/test/resources/contracts
.
然后,您可以将 Spring Cloud Contract Verifier 依赖项和插件添加到您的构建文件中,例如 以下示例显示:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-verifier</artifactId>
<scope>test</scope>
</dependency>
下面的清单显示了如何添加插件,它应该放在 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>
运行./mvnw clean install
自动生成验证应用程序的测试
遵守添加的合同。默认情况下,测试在org.springframework.cloud.contract.verifier.tests.
.
由于合同中描述的功能尚未实现 存在,则测试失败。
要使它们通过,您必须添加处理 HTTP 的正确实现
请求或消息。此外,您必须为自动生成的
tests 添加到项目中。这个类由所有自动生成的测试扩展,并且它
应包含运行它们所需的所有设置信息(例如RestAssuredMockMvc
控制器设置或消息传递测试设置)。
以下示例来自pom.xml
显示了如何指定 Base Test 类:
<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>
1 | 这baseClassForTests 元素允许您指定基本测试类。它必须是 child
的configuration 元素spring-cloud-contract-maven-plugin . |
一旦实现和测试基类就位,测试就会通过,并且 应用程序和存根工件在本地 Maven 存储库中构建和安装。 现在,您可以合并更改,并且可以发布应用程序和存根工件 在联机存储库中。
2.2. 在消费者方面
您可以使用Spring Cloud Contract Stub Runner
在集成测试中运行
模拟实际服务的 WireMock 实例或消息传送路由。
为此,请将依赖项添加到Spring Cloud Contract Stub Runner
,作为
以下示例显示:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
<scope>test</scope>
</dependency>
您可以通过以下两种方式将生产者端存根安装在 Maven 存储库中 方式:
-
签出 Producer 端存储库并添加 Contract 并生成存根 通过运行以下命令:
$ cd local-http-server-repo $ ./mvnw clean install -DskipTests
跳过测试是因为生产者端合约实现不是 ,因此自动生成的 Contract 测试会失败。 |
-
通过从远程存储库获取已经存在的生产者服务存根。为此, 将存根构件 ID 和构件存储库 URL 作为
Spring Cloud Contract Stub Runner
属性,如下例所示:stubrunner: ids: 'com.example:http-server-dsl:+:stubs:8080' repositoryRoot: https://repo.spring.io/libs-snapshot
现在,您可以使用@AutoConfigureStubRunner
.在注释中,
提供group-id
和artifact-id
的值Spring Cloud Contract Stub Runner
自
为您运行 Collaborators' Stubs,如下例所示:
@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 响应的存根版本或 预期由 Collaborator 服务发出的消息。
3. 开发您的第一个基于 Spring Cloud Contract 的应用程序
这个简短的教程将介绍如何使用 Spring Cloud Contract。它包括以下主题:
您可以在此处找到更简短的游览。
对于此示例,Stub Storage
是 Nexus/Artifactory。
下面的 UML 图显示了 Spring Cloud Contract 的各个部分之间的关系:

3.1. 在 Producer 端
开始使用Spring Cloud Contract
中,您可以添加 Spring Cloud Contract Verifier
dependency 和 plugin 添加到您的构建文件中,如下例所示:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-verifier</artifactId>
<scope>test</scope>
</dependency>
下面的清单显示了如何添加插件,它应该放在 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” 作为依赖项。这样做会引入以前的
提到的依赖项以及 ![]() |
现在,您可以使用REST/
消息传递协定
以 Groovy DSL 或 YAML 表示到 contracts 目录,该目录由contractsDslDir
财产。默认情况下,它是$rootDir/src/test/resources/contracts
.
请注意,文件名无关紧要。您可以在此
目录中。
对于 HTTP 存根,协定定义了应为 给定的请求(考虑 HTTP 方法、URL、标头、状态代码等 on) 的 S S以下示例显示了 Groovy 和 YAML 中的 HTTP 存根 Contract:
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([
fraudCheckStatus: "FRAUD",
"rejection.reason": "Amount too high"
])
headers {
contentType('application/json')
}
}
}
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 消息传递协定:
def contractDsl = Contract.make {
name "foo"
label 'some_label'
input {
messageFrom('jms:delete')
messageBody([
bookName: 'foo'
])
messageHeaders {
header('sample', 'header')
}
assertThat('bookWasDeleted()')
}
}
label: some_label
input:
messageFrom: jms:delete
messageBody:
bookName: 'foo'
messageHeaders:
sample: header
assertThat: bookWasDeleted()
运行./mvnw clean install
自动生成验证应用程序的测试
遵守添加的合同。默认情况下,生成的测试位于org.springframework.cloud.contract.verifier.tests.
.
生成的测试可能会有所不同,具体取决于您设置的框架和测试类型 在你的插件中。
在下一个列表中,您可以找到:
-
HTTP Contract 的默认测试模式
MockMvc
-
具有
JAXRS
测试模式 -
一个
WebTestClient
的测试(在使用 反应性的Web-Flux
-based applications) 与WEBTESTCLIENT
测试模式 -
基于 Spock 的测试,使用
testFramework
属性设置为SPOCK
您只需要其中一个测试框架。MockMvc 是默认值。要使用一个 ,将其库添加到您的 Classpath 中。 |
下面的清单显示了所有框架的示例:
@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");
}
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", "[email protected]")
.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");
}
}
@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");
}
given:
ContractVerifierMessage inputMessage = contractVerifierMessaging.create(
\'\'\'{"bookName":"foo"}\'\'\',
['sample': 'header']
)
when:
contractVerifierMessaging.send(inputMessage, 'jms:delete')
then:
noExceptionThrown()
bookWasDeleted()
由于合同中描述的功能尚未实现 存在,则测试失败。
要使它们通过,您必须添加处理 HTTP 的正确实现
请求或消息。此外,您必须为自动生成的
tests 添加到项目中。这个类由所有自动生成的测试扩展,并且应该
包含运行它们所需的所有设置必要信息(例如,RestAssuredMockMvc
控制器设置或消息传递测试设置)。
以下示例来自pom.xml
显示了如何指定 Base Test 类:
<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>
1 | 这baseClassForTests 元素允许您指定基本测试类。它必须是 child
的configuration 元素spring-cloud-contract-maven-plugin . |
以下示例显示了一个最小(但功能正常)的基测试类:
package com.example.contractTest;
public class BaseTestClass {
@Before
public void setup() {
RestAssuredMockMvc.standaloneSetup(new FraudController());
}
}
This minimal class really is all you need to get your tests to work. It serves as a
starting place to which the automatically generated tests attach.
Now we can move on to the implementation. For that, we first need a data class, which we
then use in our controller. The following listing shows the data class:
package com.example.Test;
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;
}
}
The preceding class provides an object in which we can store the parameters. Because the
client ID in the contract is called client.id
, we need to use the
@JsonProperty("client.id")
parameter to map it to the clientId
field.
Now we can move along to the controller, which the following listing shows:
package com.example.docTest;
@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)
}
}
}
1
We map the incoming parameters to a LoanRequest
object.
2
We check the requested loan amount to see if it is too much.
3
If it is too much, we return the JSON (created with a simple string here) that the
test expects.
4
If we had a test to catch when the amount is allowable, we could match it to this output.
The FraudController
is about as simple as things get. You can do much more, including
logging, validating the client ID, and so on.
Once the implementation and the test base class are in place, the tests pass, and both the
application and the stub artifacts are built and installed in the local Maven repository.
Information about installing the stubs jar to the local repository appears in the logs, as
the following example shows:
[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
You can now merge the changes and publish both the application and the stub artifacts
in an online repository.
3.2. On the Consumer Side
You can use Spring Cloud Contract Stub Runner in the integration tests to get a running
WireMock instance or messaging route that simulates the actual service.
To get started, add the dependency to Spring Cloud Contract Stub Runner
, as follows:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
<scope>test</scope>
</dependency>
You can get the Producer-side stubs installed in your Maven repository in either of two
ways:
-
By checking out the Producer side repository and adding contracts and generating the
stubs by running the following commands:
$ cd local-http-server-repo
$ ./mvnw clean install -DskipTests
The tests are skipped because the Producer-side contract implementation is not yet
in place, so the automatically-generated contract tests fail.
-
By getting existing producer service stubs from a remote repository. To do so,
pass the stub artifact IDs and artifact repository URL as Spring Cloud Contract Stub
Runner
properties, as the following example shows:
stubrunner:
ids: 'com.example:http-server-dsl:+:stubs:8080'
repositoryRoot: https://repo.spring.io/libs-snapshot
Now you can annotate your test class with @AutoConfigureStubRunner
. In the annotation,
provide the group-id
and artifact-id
for Spring Cloud Contract Stub Runner
to run
the collaborators' stubs for you, as the following example shows:
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment=WebEnvironment.NONE)
@AutoConfigureStubRunner(ids = {"com.example:http-server-dsl:+:stubs:6565"},
stubsMode = StubRunnerProperties.StubsMode.LOCAL)
public class LoanApplicationServiceTests {
. . .
}
Use the REMOTE
stubsMode
when downloading stubs from an online repository and
LOCAL
for offline work.
In your integration test, you can receive stubbed versions of HTTP responses or messages
that are expected to be emitted by the collaborator service. You can see entries similar
to the following in the build logs:
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}]
4. Step-by-step Guide to Consumer Driven Contracts (CDC) with Contracts on the Producer Side
Consider an example of fraud detection and the loan issuance process. The business
scenario is such that we want to issue loans to people but do not want them to steal from
us. The current implementation of our system grants loans to everybody.
Assume that Loan Issuance
is a client to the Fraud Detection
server. In the current
sprint, we must develop a new feature: if a client wants to borrow too much money,
we mark the client as a fraud.
Technical remarks
-
Fraud Detection has an artifact-id
of http-server
.
-
Loan Issuance has an artifact-id
of http-client
.
-
Both have a group-id
of com.example
.
-
For the sake of this example, the Stub Storage
is Nexus/Artifactory.
Social remarks
-
Both the client and the server development teams need to communicate directly and
discuss changes while going through the process.
-
CDC is all about communication.
The server-side code is available under Spring Cloud Contract’s repository samples/standalone/dsl/http-server
path, and the client-side code is available under Spring Cloud Contract’s repository samples/standalone/dsl/http-client
path.
In this case, the producer owns the contracts. Physically, all the contracts are
in the producer’s repository.
4.1. Technical Note
If you use the SNAPSHOT, Milestone, or Release Candidate versions, you need to add the
following section to your build:
Maven
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<!--<repository>
<id>spring-releases</id>
<name>Spring Releases</name>
<url>https://repo.spring.io/release</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>-->
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
<!--<pluginRepository>
<id>spring-releases</id>
<name>Spring Releases</name>
<url>https://repo.spring.io/release</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>-->
</pluginRepositories>
Gradle
repositories {
mavenCentral()
mavenLocal()
maven { url "https://repo.spring.io/snapshot" }
maven { url "https://repo.spring.io/milestone" }
maven { url "https://repo.spring.io/release" }
}
For simplicity, we use the following acronyms:
-
Loan Issuance (LI): The HTTP client
-
Fraud Detection (FD): The HTTP server
-
SCC: Spring Cloud Contract
4.2. The Consumer Side (Loan Issuance)
As a developer of the Loan Issuance service (a consumer of the Fraud Detection server), you might do the following steps:
-
Start doing TDD by writing a test for your feature.
-
Write the missing implementation.
-
Clone the Fraud Detection service repository locally.
-
Define the contract locally in the repository of the fraud detection service.
-
Add the Spring Cloud Contract (SCC) plugin.
-
Run the integration tests.
-
File a pull request.
-
Create an initial implementation.
-
Take over the pull request.
-
Write the missing implementation.
-
Deploy your application.
-
Work online.
We start with the loan issuance flow, which the following UML diagram shows:
4.2.1. Start Doing TDD by Writing a Test for Your Feature
The following listing shows a test that we might use to check whether a loan amount is too
large:
@Test
public void shouldBeRejectedDueToAbnormalLoanAmount() {
// given:
LoanApplication application = new LoanApplication(new Client("1234567890"),
99999);
// when:
LoanApplicationResult loanApplication = service.loanApplication(application);
// then:
assertThat(loanApplication.getLoanApplicationStatus())
.isEqualTo(LoanApplicationStatus.LOAN_APPLICATION_REJECTED);
assertThat(loanApplication.getRejectionReason()).isEqualTo("Amount too high");
}
Assume that you have written a test of your new feature. If a loan application for a big
amount is received, the system should reject that loan application with some description.
4.2.2. Write the Missing Implementation
At some point in time, you need to send a request to the Fraud Detection service. Assume
that you need to send the request containing the ID of the client and the amount the
client wants to borrow. You want to send it to the /fraudcheck
URL by using the PUT
method.
To do so, you might use code similar to the following:
ResponseEntity<FraudServiceResponse> response = restTemplate.exchange(
"http://localhost:" + port + fraudCheck(), HttpMethod.PUT,
new HttpEntity<>(request, httpHeaders), FraudServiceResponse.class);
For simplicity, the port of the Fraud Detection service is set to 8080
, and the
application runs on 8090
.
If you start the test at this point, it breaks, because no service currently runs on port
8080
.
4.2.3. Clone the Fraud Detection service repository locally
You can start by playing around with the server side contract. To do so, you must first
clone it, by running the following command:
$ git clone https://your-git-server.com/server-side.git local-http-server-repo
4.2.4. Define the Contract Locally in the Repository of the Fraud Detection Service
As a consumer, you need to define what exactly you want to achieve. You need to formulate
your expectations. To do so, write the following contract:
Place the contract in the src/test/resources/contracts/fraud
folder. The fraud
folder
is important because the producer’s test base class name references that folder.
The following example shows our contract, in both Groovy and YAML:
groovy
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package contracts
org.springframework.cloud.contract.spec.Contract.make {
request { // (1)
method 'PUT' // (2)
url '/fraudcheck' // (3)
body([ // (4)
"client.id": $(regex('[0-9]{10}')),
loanAmount : 99999
])
headers { // (5)
contentType('application/json')
}
}
response { // (6)
status OK() // (7)
body([ // (8)
fraudCheckStatus : "FRAUD",
"rejection.reason": "Amount too high"
])
headers { // (9)
contentType('application/json')
}
}
}
/*
From the Consumer perspective, when shooting a request in the integration test:
(1) - If the consumer sends a request
(2) - With the "PUT" method
(3) - to the URL "/fraudcheck"
(4) - with the JSON body that
* has a field `client.id` that matches a regular expression `[0-9]{10}`
* has a field `loanAmount` that is equal to `99999`
(5) - with header `Content-Type` equal to `application/json`
(6) - then the response will be sent with
(7) - status equal `200`
(8) - and JSON body equal to
{ "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" }
(9) - with header `Content-Type` equal to `application/json`
From the Producer perspective, in the autogenerated producer-side test:
(1) - A request will be sent to the producer
(2) - With the "PUT" method
(3) - to the URL "/fraudcheck"
(4) - with the JSON body that
* has a field `client.id` that will have a generated value that matches a regular expression `[0-9]{10}`
* has a field `loanAmount` that is equal to `99999`
(5) - with header `Content-Type` equal to `application/json`
(6) - then the test will assert if the response has been sent with
(7) - status equal `200`
(8) - and JSON body equal to
{ "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" }
(9) - with header `Content-Type` matching `application/json.*`
*/
yaml
request: # (1)
method: PUT # (2)
url: /yamlfraudcheck # (3)
body: # (4)
"client.id": 1234567890
loanAmount: 99999
headers: # (5)
Content-Type: application/json
matchers:
body:
- path: $.['client.id'] # (6)
type: by_regex
value: "[0-9]{10}"
response: # (7)
status: 200 # (8)
body: # (9)
fraudCheckStatus: "FRAUD"
"rejection.reason": "Amount too high"
headers: # (10)
Content-Type: application/json
#From the Consumer perspective, when shooting a request in the integration test:
#
#(1) - If the consumer sends a request
#(2) - With the "PUT" method
#(3) - to the URL "/yamlfraudcheck"
#(4) - with the JSON body that
# * has a field `client.id`
# * has a field `loanAmount` that is equal to `99999`
#(5) - with header `Content-Type` equal to `application/json`
#(6) - and a `client.id` json entry matches the regular expression `[0-9]{10}`
#(7) - then the response will be sent with
#(8) - status equal `200`
#(9) - and JSON body equal to
# { "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" }
#(10) - with header `Content-Type` equal to `application/json`
#
#From the Producer perspective, in the autogenerated producer-side test:
#
#(1) - A request will be sent to the producer
#(2) - With the "PUT" method
#(3) - to the URL "/yamlfraudcheck"
#(4) - with the JSON body that
# * has a field `client.id` `1234567890`
# * has a field `loanAmount` that is equal to `99999`
#(5) - with header `Content-Type` equal to `application/json`
#(7) - then the test will assert if the response has been sent with
#(8) - status equal `200`
#(9) - and JSON body equal to
# { "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" }
#(10) - with header `Content-Type` equal to `application/json`
The YML contract is quite straightforward. However, when you take a look at the contract
written with a statically typed Groovy DSL, you might wonder what the
value(client(…), server(…))
parts are. By using this notation, Spring Cloud
Contract lets you define parts of a JSON block, a URL, or other structure that is dynamic. In the case
of an identifier or a timestamp, you need not hardcode a value. You want to allow some
different ranges of values. To enable ranges of values, you can set regular expressions
that match those values for the consumer side. You can provide the body by means of either
a map notation or a String with interpolations. We highly recommend using the map notation.
To set up contracts, you must understand the map notation. See the
Groovy docs regarding JSON.
The previously shown contract is an agreement between two sides that:
-
If an HTTP request is sent with all of:
-
A PUT
method on the /fraudcheck
endpoint
-
A JSON body with a client.id
that matches the regular expression [0-9]{10}
and
loanAmount
equal to 99999
-
A Content-Type
header with a value of application/vnd.fraud.v1+json
-
Then an HTTP response is sent to the consumer that
-
Has status 200
-
Contains a JSON body with the fraudCheckStatus
field containing a value of FRAUD
and
the rejectionReason
field having a value of Amount too high
-
Has a Content-Type
header with a value of application/vnd.fraud.v1+json
Once you are ready to check the API in practice in the integration tests, you need to
install the stubs locally.
4.2.5. Add the Spring Cloud Contract Verifier Plugin
We can add either a Maven or a Gradle plugin. In this example, we show how to add Maven.
First, we add the Spring Cloud Contract
BOM, as the following example shows:
<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>
Next, add the Spring Cloud Contract Verifier
Maven plugin, as the following example shows:
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>${spring-cloud-contract.version}</version>
<extensions>true</extensions>
<configuration>
<packageWithBaseClasses>com.example.fraud</packageWithBaseClasses>
<!-- <convertToYaml>true</convertToYaml>-->
</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>
Since the plugin was added, you get the Spring Cloud Contract Verifier
features, which,
from the provided contracts:
-
Generate and run tests
-
Produce and install stubs
You do not want to generate tests, since you, as the consumer, want only to play with the
stubs. You need to skip the test generation and invokation. To do so, run the following commands:
$ cd local-http-server-repo
$ ./mvnw clean install -DskipTests
Once you run those commands, you should you see something like the following content in the logs:
[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
The following line is extremely important:
[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
It confirms that the stubs of the http-server
have been installed in the local
repository.
4.2.6. Running the Integration Tests
In order to profit from the Spring Cloud Contract Stub Runner functionality of automatic
stub downloading, you must do the following in your consumer side project (Loan
Application service
):
-
Add the Spring Cloud Contract
BOM, as follows:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud-release-train.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
-
Add the dependency to Spring Cloud Contract Stub Runner
, as follows:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
<scope>test</scope>
</dependency>
-
Annotate your test class with @AutoConfigureStubRunner
. In the annotation, provide the
group-id
and artifact-id
for the Stub Runner to download the stubs of your
collaborators.
@SpringBootTest(webEnvironment = WebEnvironment.NONE)
@AutoConfigureStubRunner(ids = {
"com.example:http-server-dsl:0.0.1:stubs"}, stubsMode = StubRunnerProperties.StubsMode.LOCAL)
public class LoanApplicationServiceTests {
-
(Optional) Because you are playing with the collaborators offline, you
can also provide the offline work switch (StubRunnerProperties.StubsMode.LOCAL
).
Now, when you run your tests, you see something like the following output in the logs:
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}]
This output means that Stub Runner has found your stubs and started a server for your application
with a group ID of com.example
and an artifact ID of http-server
with version 0.0.1-SNAPSHOT
of
the stubs and with the stubs
classifier on port 8080
.
4.2.7. Filing a Pull Request
What you have done until now is an iterative process. You can play around with the
contract, install it locally, and work on the consumer side until the contract works as
you wish.
Once you are satisfied with the results and the test passes, you can publish a pull request to
the server side. Currently, the consumer side work is done.
4.3. The Producer Side (Fraud Detection server)
As a developer of the Fraud Detection server (a server to the Loan Issuance service), you
might want to:
-
Take over the pull request
-
Write the missing implementation
-
Deploy the application
The following UML diagram shows the fraud detection flow:
4.3.1. Taking over the Pull Request
As a reminder, the following listing shows the initial implementation:
@RequestMapping(value = "/fraudcheck", method = PUT)
public FraudCheckResult fraudCheck(@RequestBody FraudCheck fraudCheck) {
return new FraudCheckResult(FraudCheckStatus.OK, NO_REASON);
}
Then you can run the following commands:
$ git checkout -b contract-change-pr master
$ git pull https://your-git-server.com/server-side-fork.git contract-change-pr
You must add the dependencies needed by the autogenerated tests, as follows:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-verifier</artifactId>
<scope>test</scope>
</dependency>
In the configuration of the Maven plugin, you must pass the packageWithBaseClasses
property, as follows:
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>${spring-cloud-contract.version}</version>
<extensions>true</extensions>
<configuration>
<packageWithBaseClasses>com.example.fraud</packageWithBaseClasses>
<!-- <convertToYaml>true</convertToYaml>-->
</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>
This example uses “convention-based” naming by setting the
packageWithBaseClasses
property. Doing so means that the two last packages combine to
make the name of the base test class. In our case, the contracts were placed under
src/test/resources/contracts/fraud
. Since you do not have two packages starting from
the contracts
folder, pick only one, which should be fraud
. Add the Base
suffix and
capitalize fraud
. That gives you the FraudBase
test class name.
All the generated tests extend that class. Over there, you can set up your Spring Context
or whatever is necessary. In this case, you should use Rest Assured MVC to
start the server side FraudDetectionController
. The following listing shows the
FraudBase
class:
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.fraud;
public class FraudBase {
@BeforeEach
public void setup() {
RestAssuredMockMvc.standaloneSetup(new FraudDetectionController(),
new FraudStatsController(stubbedStatsProvider()));
}
private StatsProvider stubbedStatsProvider() {
return fraudType -> {
switch (fraudType) {
case DRUNKS:
return 100;
case ALL:
return 200;
}
return 0;
};
}
public void assertThatRejectionReasonIsNull(Object rejectionReason) {
assert rejectionReason == null;
}
}
Now, if you run the ./mvnw clean install
, you get something like the following output:
Results :
Tests in error:
ContractVerifierTest.validate_shouldMarkClientAsFraud:32 » IllegalState Parsed...
This error occurs because you have a new contract from which a test was generated, and it
failed since you have not implemented the feature. The auto-generated test would look
like the following test method:
@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");
}
If you used the Groovy DSL, you can see that all the producer()
parts of the Contract that were present in the
value(consumer(…), producer(…))
blocks got injected into the test.
If you use YAML, the same applies for the matchers
sections of the response
.
Note that, on the producer side, you are also doing TDD. The expectations are expressed
in the form of a test. This test sends a request to our own application with the URL,
headers, and body defined in the contract. It also expects precisely defined values
in the response. In other words, you have the red
part of red
, green
, and
refactor
. It is time to convert the red
into the green
.
4.3.2. Write the Missing Implementation
Because you know the expected input and expected output, you can write the missing
implementation as follows:
@RequestMapping(value = "/fraudcheck", method = PUT)
public FraudCheckResult fraudCheck(@RequestBody FraudCheck fraudCheck) {
if (amountGreaterThanThreshold(fraudCheck)) {
return new FraudCheckResult(FraudCheckStatus.FRAUD, AMOUNT_TOO_HIGH);
}
return new FraudCheckResult(FraudCheckStatus.OK, NO_REASON);
}
When you run ./mvnw clean install
again, the tests pass. Since the Spring Cloud
Contract Verifier plugin adds the tests to the generated-test-sources
, you can
actually run those tests from your IDE.
4.3.3. Deploying Your Application
Once you finish your work, you can deploy your changes. To do so, you must first merge the
branch by running the following commands:
$ git checkout master
$ git merge --no-ff contract-change-pr
$ git push origin master
Your CI might run a command such as ./mvnw clean deploy
, which would publish both the
application and the stub artifacts.
4.4. Consumer Side (Loan Issuance), Final Step
As a developer of the loan issuance service (a consumer of the Fraud Detection server), you need to:
-
Merge our feature branch to master
-
Switch to online mode of working
The following UML diagram shows the final state of the process:
4.4.1. Merging a Branch to Master
The following commands show one way to merge a branch into master with Git:
$ git checkout master
$ git merge --no-ff contract-change-pr
4.4.2. Working Online
Now you can disable the offline work for Spring Cloud Contract Stub Runner and indicate
where the repository with your stubs is located. At this moment, the stubs of the server
side are automatically downloaded from Nexus/Artifactory. You can set the value of
stubsMode
to REMOTE
. The following code shows an example of
achieving the same thing by changing the properties:
stubrunner:
ids: 'com.example:http-server-dsl:+:stubs:8080'
repositoryRoot: https://repo.spring.io/libs-snapshot
That’s it. You have finished the tutorial.
5. Next Steps
Hopefully, this section provided some of the Spring Cloud Contract basics and got you on your way
to writing your own applications. If you are a task-oriented type of developer, you might
want to jump over to spring.io and check out some
getting started guides that solve specific “How do I do that
with Spring?” problems. We also have Spring Cloud Contract-specific
“how-to” reference documentation.
Otherwise, the next logical step is to read Using Spring Cloud Contract. If
you are really impatient, you could also jump ahead and read about
Spring Cloud Contract features.
In addition, you can check out the following videos:
-
"Consumer Driven Contracts and Your Microservice Architecture" by Olga Maciaszek-Sharma and Marcin Grzejszczak
-
"Contract Tests in the Enterprise" by Marcin Grzejszczak
-
"Why Contract Tests Matter?" by Marcin Grzejszczak
You can find the default project samples at
samples.