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 定义用于生成以下资源:

89.1 为什么选择 Contract Verifier?

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

89.1.1 测试问题

如果我们想测试左上角的 application 以确定它是否可以与其他服务通信,我们可以做以下两件事之一:

两者都有其优点,但也有许多缺点。

部署所有微服务并执行端到端测试

好处:

缺点:

在 unit/integration 测试中模拟其他微服务

好处:

缺点:

为了解决上述问题,创建了带有 Stub Runner 的 Spring Cloud Contract Verifier。主要的 idea 是为您提供非常快速的反馈,而无需设置整个微服务世界。如果您处理存根,那么您需要的唯一应用程序是您的 application 直接使用的那些应用程序。

Spring Cloud Contract Verifier 可以确保您使用的存根是由您正在调用的服务创建的。此外,如果您可以使用它们,则意味着它们是针对 producer 的一方进行测试的。简而言之,您可以信任这些存根。

89.2 目的

使用 Stub Runner 的 Spring Cloud Contract Verifier 的主要目的是:

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 存根:

$ cd local-http-server-repo
$ ./mvnw clean install -DskipTests

正在跳过测试,因为 Producer-side contract implementation 尚未到位,因此 automatically-generated contract 测试失败。

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-idartifact-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-RSEXPLICIT,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 存根:

$ cd local-http-server-repo
$ ./mvnw clean install -DskipTests

跳过测试是因为 Producer-side contract implementation 尚未到位,因此 automatically-generated contract 测试失败。

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

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

@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 是双方之间达成的协议:

一旦准备好在 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:

您不希望生成测试,因为您作为 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-idartifact-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 的情况下,同样适用于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。

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。

首页