89. Spring Cloud Contract Verifier 简介
Accurest 项目最初由 Marcin Grzejszczak 和 Jakub Kubrynski 创建(codearte.io)
Spring Cloud Contract Verifier 启用 Consumer Driven Contract(CDC)开发 JVM-based applications。它将 TDD 移动到软件 architecture 的 level。
Spring Cloud Contract Verifier 附带 Contract 定义语言(CDL)。 Contract 定义用于生成以下资源:
-
在 client code(client tests)上进行 integration 测试时,WireMock 将使用 JSON 存根定义。 Test code 仍然必须手工编写,测试数据由 Spring Cloud Contract Verifier 生成。
-
如果您正在使用消息传递服务,则消息 routes。我们与 Spring Integration,Spring Cloud Stream,Spring AMQP 和 Apache Camel 集成。您还可以设置自己的集成。
-
验证测试(在 JUnit 或 Spock 中)用于验证 API 的 server-side 实现是否符合 contract(服务器测试)。 Spring Cloud Contract Verifier 生成完整测试。
89.1 为什么选择 Contract Verifier?
假设我们有一个由多个微服务组成的系统:
89.1.1 测试问题
如果我们想测试左上角的 application 以确定它是否可以与其他服务通信,我们可以做以下两件事之一:
-
部署所有微服务并执行 end-to-end 测试。
-
Mock 中的其他微服务测试。
两者都有其优点,但也有许多缺点。
部署所有微服务并执行端到端测试
好处:
-
模拟 production。
-
测试服务之间的真实通信。
缺点:
-
为了测试一个微服务,我们必须部署 6 个微服务,几个数据库等。
-
测试 run 被锁定用于单个测试套件的环境(在此期间没有其他人能够运行测试)。
-
他们需要 long time 来运行 run。
-
反馈在 process 中很晚。
-
它们非常难以调试。
在 unit/integration 测试中模拟其他微服务
好处:
-
它们提供非常快速的反馈。
-
他们没有基础设施要求。
缺点:
-
该服务的实现者创建可能与现实无关的存根。
-
您可以通过传递测试和生产失败来进行 production。
为了解决上述问题,创建了带有 Stub Runner 的 Spring Cloud Contract Verifier。主要的 idea 是为您提供非常快速的反馈,而无需设置整个微服务世界。如果您处理存根,那么您需要的唯一应用程序是您的 application 直接使用的那些应用程序。
Spring Cloud Contract Verifier 可以确保您使用的存根是由您正在调用的服务创建的。此外,如果您可以使用它们,则意味着它们是针对 producer 的一方进行测试的。简而言之,您可以信任这些存根。
89.2 目的
使用 Stub Runner 的 Spring Cloud Contract Verifier 的主要目的是:
-
确保 WireMock/Messaging 存根(在 developing client 时使用)完全按照实际的 server-side implementation 执行。
-
推广 ATDD 方法和微服务架构风格。
-
提供一种方法来发布 contracts 中双方立即可见的更改。
-
生成样板测试 code 以在服务器端使用。
Spring Cloud Contract Verifier 的目的不是开始在 contracts 中编写业务 features。假设我们有欺诈检查的业务用例。如果用户出于 100 种不同的原因可能是欺诈行为,我们会假设你会创建 2 个 contracts,一个用于肯定案例,一个用于负面案例。 Contract 测试用于测试 applications 之间的 contracts 而不是模拟完整行为。
89.3 如何运作
本节将探讨如何使用 Stub Runner 运行 Spring Cloud Contract Verifier。
89.3.1 A Three-second 巡回赛
这个非常简短的游览将使用 Spring Cloud Contract:
你可以找到一个更长的旅程这里。
在 Producer 侧
要开始使用 Spring Cloud Contract,请将带有REST/
messaging contracts 的 files 与 Groovy DSL 或 YAML 一起添加到 contracts 目录,该目录由contractsDslDir
property 设置。默认情况下,它是$rootDir/src/test/resources/contracts
。
然后将 Spring Cloud Contract Verifier 依赖项和插件添加到 build 文件中,如下面的示例所示:
<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>
Running ./mvnw clean install
自动生成测试,以验证 application 是否符合添加的 contracts。默认情况下,测试在org.springframework.cloud.contract.verifier.tests.
下生成。
由于 contracts 描述的功能的 implementation 尚未出现,测试失败。
要使它们通过,您必须添加处理 HTTP 请求或消息的正确 implementation。此外,您必须为项目添加正确的基础测试 class 以进行 auto-generated 测试。所有 auto-generated 测试都扩展了这个 class,它应该包含 run 它们所需的所有设置(用于 example RestAssuredMockMvc
控制器设置或消息传递测试设置)。
一旦 implementation 和 test base class 到位,测试就会通过,application 和 stub artifacts 都会构建并安装在本地 Maven repository 中。现在可以合并这些更改,并且 application 和 stub artifacts 都可以在 online repository 中发布。
在 Consumer Side
可以在 integration 测试中使用Spring Cloud Contract Stub Runner
来获取模拟实际服务的 running WireMock 实例或消息传递 route。
为此,请将依赖项添加到Spring Cloud Contract Stub Runner
,如以下 example 所示:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
<scope>test</scope>
</dependency>
您可以通过以下两种方式之一获取 Maven repository 中安装的 Producer-side 存根:
- 通过签出 Producer side repository 并通过 running 以下命令添加 contracts 并生成存根:
$ cd local-http-server-repo
$ ./mvnw clean install -DskipTests
正在跳过测试,因为 Producer-side contract implementation 尚未到位,因此 automatically-generated contract 测试失败。
- 通过从 remote repository 获取 already-existing producer 服务存根。为此,请将 stub artifact ID 和 artifact repository URL 作为
Spring Cloud Contract Stub Runner
properties 传递,如以下 example 所示:
stubrunner:
ids: 'com.example:http-server-dsl:+:stubs:8080'
repositoryRoot: http://repo.spring.io/libs-snapshot
现在,您可以使用@AutoConfigureStubRunner
注释 test class。在 annotation 中,为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 {
从在线 repository 下载存根时使用
REMOTE
stubsMode
,在离线工作时使用LOCAL
。
现在,在 integration 测试中,您可以接收预期由协作者服务发出的 HTTP 响应或消息的存根版本。
89.3.2 A Three-minute 巡回赛
这个简短的游览将介绍使用 Spring Cloud Contract:
你可以找到一个更简短的旅游这里。
在 Producer 侧
要开始使用Spring Cloud Contract
,请将带有REST/
messaging contracts 的 files 添加到 contracts 目录中,该目录由 Groovy DSL 或 YAML 表示,该目录由contractsDslDir
property 设置。默认情况下,它是$rootDir/src/test/resources/contracts
。
对于 HTTP 存根,contract 定义应为给定请求返回何种响应(考虑 HTTP 方法,URL,headers,状态代码等)。以下 example 显示了 Groovy DSL 中的 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')
}
}
}
在 YAML 中表达的相同 contract 看起来像以下 example:
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
在消息传递的情况下,您可以定义:
-
可以定义输入和输出消息(考虑发送它和发送的位置,消息正文和标题)。
-
收到消息后应调用的方法。
-
调用时应该触发消息的方法。
以下 example 显示了在 Groovy DSL 中表示的 Camel 消息传递 contract:
def contractDsl = Contract.make {
label 'some_label'
input {
messageFrom('jms:delete')
messageBody([
bookName: 'foo'
])
messageHeaders {
header('sample', 'header')
}
assertThat('bookWasDeleted()')
}
}
以下 example 显示了在 YAML 中表示的相同 contract:
label: some_label
input:
messageFrom: jms:delete
messageBody:
bookName: 'foo'
messageHeaders:
sample: header
assertThat: bookWasDeleted()
然后,您可以将 Spring Cloud Contract Verifier 依赖项和插件添加到 build 文件中,如下面的示例所示:
<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>
Running ./mvnw clean install
自动生成测试,以验证 application 是否符合添加的 contracts。默认情况下,生成的测试位于org.springframework.cloud.contract.verifier.tests.
下。
以下 example 显示了 HTTP contract 的 sample auto-generated 测试:
@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");
}
前面的 example 使用 Spring 的MockMvc
来运行测试。这是 HTTP contracts 的默认测试模式。但是,也可以使用 JAX-RX client 和显式 HTTP 调用。 (为此,请将插件的testMode
property 更改为JAX-RS
或EXPLICIT
,respectively.)
除了默认的 JUnit 之外,您还可以通过将插件testFramework
property 设置为Spock
来使用 Spock 测试。
您现在还可以基于 contracts 生成 WireMock 方案,方法是在 contract 文件名的开头包含 order 数字后跟下划线。
以下 example 显示了 Spock 中针对消息传递存根 contract 的 auto-generated 测试:
[source,groovy,indent=0]
given:
ContractVerifierMessage inputMessage = contractVerifierMessaging.create(
\'\'\'{"bookName":"foo"}\'\'\',
['sample': 'header']
)
when:
contractVerifierMessaging.send(inputMessage, 'jms:delete')
then:
noExceptionThrown()
bookWasDeleted()
由于 contracts 描述的功能的 implementation 尚未出现,测试失败。
要使它们通过,您必须添加正确的 implementation 来处理 HTTP 请求或消息。此外,您必须为项目添加正确的基础测试 class 以进行 auto-generated 测试。这个 class 由所有 auto-generated 测试扩展,并且应包含运行它们所需的所有设置(用于 example,RestAssuredMockMvc
控制器设置或消息传递测试设置)。
一旦 implementation 和 test base class 到位,测试就会通过,application 和 stub artifacts 都会构建并安装在本地 Maven repository 中。有关将存根 jar 安装到本地 repository 的信息将显示在日志中,如以下 example 所示:
[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
您现在可以合并更改并在 online repository 中发布 application 和 stub artifacts。
Docker 项目
在 order 中,在 non-JVM 技术中 creating applications 时启用 contracts,已创建springcloud/spring-cloud-contract
Docker 镜像。它包含一个自动为 HTTP contracts 生成测试的项目,并在EXPLICIT
测试模式下执行它们。然后,如果测试通过,它会生成 Wiremock 存根,并且可选地将它们发布到 artifact manager。在 order 中使用图像,您可以将 contracts 挂载到/contracts
目录并设置一些环境变量。
在 Consumer Side
可以在 integration 测试中使用Spring Cloud Contract Stub Runner
来获取模拟实际服务的 running WireMock 实例或消息传递 route。
要开始,请将依赖项添加到Spring Cloud Contract Stub Runner
:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
<scope>test</scope>
</dependency>
您可以通过以下两种方式之一获取 Maven repository 中安装的 Producer-side 存根:
- 通过签出 Producer side repository 并通过 running 以下命令添加 contracts 并生成存根:
$ cd local-http-server-repo
$ ./mvnw clean install -DskipTests
跳过测试是因为 Producer-side contract implementation 尚未到位,因此 automatically-generated contract 测试失败。
- 从 remote repository 获取已存在的 producer 服务存根。为此,将 stub artifact ID 和 artifact repository URl 作为
Spring Cloud Contract Stub Runner
properties 传递,如下面的 example 所示:
stubrunner:
ids: 'com.example:http-server-dsl:+:stubs:8080'
repositoryRoot: http://repo.spring.io/libs-snapshot
现在,您可以使用@AutoConfigureStubRunner
注释 test class。在 annotation 中,为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 {
从在线 repository 下载存根时使用
REMOTE
stubsMode
,在离线工作时使用LOCAL
。
在 integration 测试中,您可以接收预期由协作者服务发出的 HTTP 响应或消息的存根版本。您可以在 build 日志中看到类似于以下内容的条目:
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}]
89.3.3 定义 Contract
作为服务的消费者,我们需要定义我们想要实现的目标。我们需要制定我们的期望。这就是我们写 contracts 的原因。
假设您要发送包含 client 公司 ID 的请求以及它要从我们借入的金额。您还希望通过 PUT 方法将其发送到/fraudcheck url。
Groovy DSL.
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: /fraudcheck # (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;charset=UTF-8
#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`
# * 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 "/fraudcheck"
#(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;charset=UTF-8`
89.3.4 Client Side
Spring Cloud Contract 生成存根,您可以在 client-side 测试期间使用它们。你得到一个模拟服务的 running WireMock instance/Messaging route。您希望使用适当的存根定义来提供该实例。
在 time 中的某个时刻,您需要向欺诈检测服务发送请求。
ResponseEntity<FraudServiceResponse> response =
restTemplate.exchange("http://localhost:" + port + "/fraudcheck", HttpMethod.PUT,
new HttpEntity<>(request, httpHeaders),
FraudServiceResponse.class);
使用@AutoConfigureStubRunner
注释您的测试 class。在 annotation 中为 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 {
之后,在测试期间,Spring Cloud Contract 会自动在 Maven repository 中找到存根(模拟真实服务)并在配置的(或随机的)port 上公开它们。
89.3.5 服务器端
由于你正在开发你的存根,你需要确保它实际上类似于你的具体 implementation。你不能遇到存根以一种方式运行而你的 application 以不同方式运行的情况,尤其是在 production 中。
要确保 application 的行为与您在存根中定义的方式相同,测试将从您提供的存根中生成。
自动生成的测试或多或少看起来像这样:
@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");
}
89.4 Step-by-step Consumer Driven Contracts(CDC)指南
考虑一个欺诈检测和贷款发放 process 的例子。业务场景是这样的,我们想向人们发放贷款,但不希望他们从我们这里偷窃。我们系统目前的实施向每个人发放贷款。
假设Loan Issuance
是Fraud Detection
服务器的客户端。在当前的冲刺中,我们必须开发一个新的 feature:如果客户想要借太多钱,那么我们将 client 标记为欺诈。
技术评论 - 欺诈检测的artifact-id
为http-server
,而贷款发行的 artifact-id 为http-client
,两者的group-id
为com.example
。
社交评论 - 客户端和服务器开发团队需要直接沟通并在进行 process 时讨论变更。 CDC 就是沟通。
服务器端 code 可在此处获得和client code 在这里。
在这种情况下,producer 拥有 contracts。在物理上,所有 contract 都在 producer 的 repository 中。
89.4.1 技术说明
如果使用SNAPSHOT/Milestone/Release Candidate版本,请将以下部分添加到您的 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>
摇篮.
repositories {
mavenCentral()
mavenLocal()
maven { url "http://repo.spring.io/snapshot" }
maven { url "http://repo.spring.io/milestone" }
maven { url "http://repo.spring.io/release" }
}
89.4.2 消费者方(贷款发行)
作为贷款发放服务的开发者(欺诈检测服务器的 consumer),您可以执行以下步骤:
-
通过为 feature 编写测试来开始执行 TDD。
-
写下缺少的 implementation。
-
在本地克隆欺诈检测服务 repository。
-
在 repo 欺诈检测服务中本地定义 contract。
-
添加 Spring Cloud Contract Verifier 插件。
-
运行 integration 测试。
-
提交拉取请求。
-
创建一个初始 implementation。
-
接管拉取请求。
-
写下缺少的 implementation。
-
部署您的应用。
-
在线工作。
通过为 feature 编写测试来开始进行 TDD.
@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");
}
假设您已经编写了对新 feature 的测试。如果收到大额贷款申请,系统应拒绝该贷款应用程序并附上一些描述。
写下缺少的 implementation.
在 time 中的某个时刻,您需要向欺诈检测服务发送请求。假设您需要发送包含 client ID 和 client 想要借用的金额的请求。您想通过PUT
方法将其发送到/fraudcheck
url。
ResponseEntity<FraudServiceResponse> response =
restTemplate.exchange("http://localhost:" + port + "/fraudcheck", HttpMethod.PUT,
new HttpEntity<>(request, httpHeaders),
FraudServiceResponse.class);
为简单起见,欺诈检测服务的 port 设置为8080
,application 运行在8090
上。
如果此时开始测试,它会中断,因为当前没有服务在 port 8080
上运行。
在本地克隆欺诈检测服务存储库.
您可以从服务器端 contract 开始。为此,您必须先克隆它。
$ git clone https://your-git-server.com/server-side.git local-http-server-repo
在欺诈检测服务的 repo 中本地定义 contract.
作为 consumer,您需要定义您想要实现的目标。你需要制定你的期望。为此,请编写以下 contract:
将 contract 放在
src/test/resources/contracts/fraud
文件夹下。fraud
文件夹很重要,因为 producer 的测试 base class name references 那个文件夹。
Groovy DSL.
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: /fraudcheck # (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;charset=UTF-8
#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`
# * 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 "/fraudcheck"
#(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;charset=UTF-8`
YML contract 非常 straight-forward。但是当你看一下使用静态类型的 Groovy DSL 编写的 Contract 时 - 你可能想知道value(client(…), server(…))
部分是什么。通过使用此表示法,Spring Cloud Contract 允许您定义动态的 JSON 块,URL 等部分。如果是标识符或时间戳,则无需对 value 进行硬编码。您希望允许一些不同的值范围。要启用值范围,可以设置与 consumer 端的值匹配的正则表达式。您可以通过 map 表示法或带插值的 String 来提供正文。有关更多信息,请参阅第 95 章,Contract DSL部分。我们强烈建议使用 map 表示法!
您必须了解 order 中的 map 符号才能设置 contracts。请阅读关于 JSON 的 Groovy docs。
之前显示的 contract 是双方之间达成的协议:
-
如果 HTTP 请求与所有的一起发送
-
/fraudcheck
端点上的PUT
方法, -
一个 JSON 主体,其
client.id
匹配正则表达式[0-9]{10}
,loanAmount
等于99999
, -
和一个_val为
application/vnd.fraud.v1+json
的Content-Type
标题, -
然后将 HTTP 响应发送给 consumer
-
状态
200
, -
包含一个 JSON 主体,其
fraudCheckStatus
字段包含 valueFRAUD
,rejectionReason
字段包含 valueAmount too high
, -
和一个标题,value 为
application/vnd.fraud.v1+json
。
一旦准备好在 integration 测试中检查 API,您需要在本地安装存根。
添加 Spring Cloud Contract Verifier 插件.
我们可以添加 Maven 或 Gradle 插件。在这个 example 中,您将看到如何添加 Maven。首先,添加Spring Cloud Contract
BOM。
<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 Verifier
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>com.example.fraud</packageWithBaseClasses>
</configuration>
</plugin>
自添加插件后,您将从提供的 contracts 中获取Spring Cloud Contract Verifier
features:
-
生成和 run 测试
-
生成并安装存根
您不希望生成测试,因为您作为 consumer 只想使用存根。您需要跳过测试生成和执行。执行时:
$ cd local-http-server-repo
$ ./mvnw clean install -DskipTests
在日志中,您会看到如下内容:
[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
以下 line 非常重要:
[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
它确认http-server
的存根已安装在本地 repository 中。
运行 integration 测试.
为了从自动存根下载的 Spring Cloud Contract Stub Runner 功能中获利,您必须在 consumer 端项目(Loan Application service
)中执行以下操作:
添加Spring Cloud Contract
BOM:
<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>
将依赖项添加到Spring Cloud Contract Stub Runner
:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
<scope>test</scope>
</dependency>
使用@AutoConfigureStubRunner
注释您的测试 class。在 annotation 中,为 Stub Runner 提供group-id
和artifact-id
以下载协作者的存根。 (可选 step)因为您正在离线协作者,所以您还可以提供离线工作开关(StubRunnerProperties.StubsMode.LOCAL
)。
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment=WebEnvironment.NONE)
@AutoConfigureStubRunner(ids = {"com.example:http-server-dsl:+:stubs:6565"},
stubsMode = StubRunnerProperties.StubsMode.LOCAL)
public class LoanApplicationServiceTests {
现在,当你运行测试时,你会看到如下内容:
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}]
此输出表示 Stub Runner 已找到您的存根并为您的应用启动了一个服务器,其中包含 group id com.example
,artifact id http-server
和 version 0.0.1-SNAPSHOT
的存根以及 port 8080
上的stubs
classifier。
提交拉取请求.
你到目前为止所做的是一个迭代的 process。您可以使用 contract,在本地安装它,并在 consumer 端工作,直到 contract 按您的意愿工作。
一旦您对结果和测试通过感到满意,就向服务器端发布一个拉取请求。目前,consumer 方面的工作已经完成。
89.4.3 Producer side(欺诈检测服务器)
作为欺诈检测服务器(Loan Issuance 服务的服务器)的开发人员:
创建一个初始 implementation.
提醒一下,您可以在此处查看初始 implementation:
@RequestMapping(value = "/fraudcheck", method = PUT)
public FraudCheckResult fraudCheck(@RequestBody FraudCheck fraudCheck) {
return new FraudCheckResult(FraudCheckStatus.OK, NO_REASON);
}
接管拉取请求.
$ git checkout -b contract-change-pr master
$ git pull https://your-git-server.com/server-side-fork.git contract-change-pr
您必须添加自动生成的测试所需的依赖项:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-verifier</artifactId>
<scope>test</scope>
</dependency>
在 Maven 插件的 configuration 中,传递packageWithBaseClasses
property
<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>
</configuration>
</plugin>
此 example 通过设置
packageWithBaseClasses
property 使用“基于约定”命名。这样做意味着最后两个包组合在一起,形成基本测试 class 的 name。在我们的例子中,contracts 被置于src/test/resources/contracts/fraud
之下。由于您没有从contracts
文件夹开始的两个包,因此只选择一个,应该是fraud
。添加Base
后缀并将fraud
大写。这会给你FraudBase
test class name。
所有生成的测试都扩展了 class。在那里,您可以设置 Spring Context 或任何必要的内容。在这种情况下,使用Rest Assured MVC启动服务器端FraudDetectionController
。
package com.example.fraud;
import org.junit.Before;
import io.restassured.module.mockmvc.RestAssuredMockMvc;
public class FraudBase {
@Before
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;
}
}
现在,如果你运行./mvnw clean install
,你得到这样的东西:
Results :
Tests in error:
ContractVerifierTest.validate_shouldMarkClientAsFraud:32 » IllegalState Parsed...
发生此错误的原因是您有一个新的 contract,从中生成了测试,并且由于您尚未实现 feature 而失败。 auto-generated 测试看起来像这样:
@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");
}
如果您使用了 Groovy DSL,您可以看到,value(consumer(…), producer(…))
块中存在的 Contract 的所有producer()
部分都被注入到测试中。在使用 YAML 的情况下,同样适用于response
的matchers
部分。
请注意,在 producer 端,您也在进行 TDD。期望以测试的形式表达。此测试使用 contract 中定义的 URL,headers 和 body 向我们自己的 application 发送请求。它还期望在响应中精确定义的值。换句话说,你有red
,green
和refactor
的red
部分。将red
转换为green
是 time。
写下缺少的 implementation.
因为您知道预期的输入和预期输出,所以可以编写缺少的 implementation:
@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);
}
再次执行./mvnw clean install
时,测试通过。由于Spring Cloud Contract Verifier
插件将测试添加到generated-test-sources
,您实际上可以从 IDE 中运行这些测试。
部署您的应用程序.
完成工作后,您可以部署更改。首先,合并分支:
$ git checkout master
$ git merge --no-ff contract-change-pr
$ git push origin master
您的 CI 可能会运行./mvnw clean deploy
之类的东西,它会发布 application 和 stub artifacts。
89.4.4 Consumer Side(Loan Issuance)决赛 Step
作为贷款发放服务的开发者(欺诈检测服务器的 consumer):
合并分支到 master.
$ git checkout master
$ git merge --no-ff contract-change-pr
在线工作.
现在,您可以禁用 Spring Cloud Contract Stub Runner 的离线工作,并指明存根与存根的位置。在此 moment 中,服务器端的存根将自动从 Nexus/Artifactory 下载。您可以将stubsMode
的 value 设置为REMOTE
。以下 code 显示了通过更改 properties 实现相同功能的示例。
stubrunner:
ids: 'com.example:http-server-dsl:+:stubs:8080'
repositoryRoot: http://repo.spring.io/libs-snapshot
而已!
89.5 依赖关系
添加依赖项的最佳方法是使用正确的starter
依赖项。
对于stub-runner
,请使用spring-cloud-starter-stub-runner
。使用插件时,添加spring-cloud-starter-contract-verifier
。
89.6 其他链接
以下是与 Spring Cloud Contract Verifier 和 Stub Runner 相关的一些资源。请注意,有些可能已过时,因为 Spring Cloud Contract Verifier 项目正在不断开发中。
89.6.1 Spring Cloud Contract video
您可以查看华沙 JUG 关于 Spring Cloud Contract 的视频:
89.6.2 读物
89.7 Samples
您可以在samples找到一些 samples。