On this page
81. 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 生成完整测试。 
81.1 为什么选择 Contract Verifier?
假设我们有一个由多个微服务组成的系统:

81.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 的一方进行测试的。简而言之,您可以信任这些存根。
81.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 而不是模拟完整行为。
81.3 如何运作
本节将探讨如何使用 Stub Runner 运行 Spring Cloud Contract Verifier。
81.3.1 定义 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 200 // (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`
81.3.2 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"}, workOffline = true)
public class LoanApplicationServiceTests {
之后,在测试期间,Spring Cloud Contract 会自动在 Maven repository 中找到存根(模拟真实服务)并在配置的(或随机的)port 上公开它们。
81.3.3 服务器端
由于你正在开发你的存根,你需要确保它实际上类似于你的具体 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");
}
81.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 中。
81.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" }
}
81.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 200 // (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 来提供正文。 有关更多信息,请参阅 docs。我们强烈建议使用 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字段包含 value- FRAUD,- rejectionReason字段包含 value- Amount 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-dependencies.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-dependencies.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)因为您正在离线协作者,所以您还可以提供离线工作开关。
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment=WebEnvironment.NONE)
@AutoConfigureStubRunner(ids = {"com.example:http-server-dsl:+:stubs:6565"}, workOffline = true)
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 方面的工作已经完成。
81.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 通过设置
packageWithBaseClassesproperty 使用“基于约定”命名。这样做意味着最后两个包组合在一起,形成基本测试 class 的 name。在我们的例子中,contracts 被置于src/test/resources/contracts/fraud之下。由于您没有从contracts文件夹开始的两个包,因此只选择一个,应该是fraud。添加Base后缀并将fraud大写。这会给你FraudBasetest 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。
81.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 下载。您可以在 annotation 中关闭workOffline参数的 value。以下 code 显示了通过更改 properties 实现相同功能的示例。
stubrunner:
  ids: 'com.example:http-server-dsl:+:stubs:8080'
  repositoryRoot: http://repo.spring.io/libs-snapshot
而已!
81.5 依赖关系
添加依赖项的最佳方法是使用正确的starter依赖项。
对于stub-runner,请使用spring-cloud-starter-stub-runner。使用插件时,添加spring-cloud-starter-contract-verifier。
81.6 其他链接
以下是与 Spring Cloud Contract Verifier 和 Stub Runner 相关的一些资源。请注意,有些可能已过时,因为 Spring Cloud Contract Verifier 项目正在不断开发中。
81.6.1 Spring Cloud Contract video
您可以查看华沙 JUG 关于 Spring Cloud Contract 的视频:
81.6.2 读物
81.7 Samples
您可以在samples找到一些 samples。