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?

假设我们有一个由多个微服务组成的系统:

微服务 Architecture

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 直接使用的那些应用程序。

Stubbed 服务

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 IssuanceFraud Detection服务器的客户端。在当前的冲刺中,我们必须开发一个新的 feature:如果客户想要借太多钱,那么我们将 client 标记为欺诈。

技术评论 - 欺诈检测的artifact-idhttp-server,而贷款发行的 artifact-id 为http-client,两者的group-idcom.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+jsonContent-Type标题,

  • 然后将 HTTP 响应发送给 consumer

  • 状态200

  • 包含一个 JSON 主体,其fraudCheckStatus字段包含 value FRAUDrejectionReason字段包含 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-idartifact-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 通过设置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 的情况下,同样适用于responsematchers部分。

请注意,在 producer 端,您也在进行 TDD。期望以测试的形式表达。此测试使用 contract 中定义的 URL,headers 和 body 向我们自己的 application 发送请求。它还期望在响应中精确定义的值。换句话说,你有redgreenrefactorred部分。将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。