Developing Your First Spring Cloud Contract-based Application

此简要指南介绍如何使用 Spring Cloud Contract。它包含以下主题:

This brief tour walks through using Spring Cloud Contract. It consists of the following topics:

您可在 here 中找到更简洁的教程。

You can find an even more brief tour here.

为了这个示例,“存根存储”是 Nexus/Artifactory。

For the sake of this example, the Stub Storage is Nexus/Artifactory.

以下 UML 图表显示了 Spring Cloud Contract 的各部分之间的关系:

The following UML diagram shows the relationship of the parts of Spring Cloud Contract:

"API Producer"->"API Producer": add Spring Cloud \nContract (SCC) plugin
"API Producer"->"API Producer": add SCC Verifier dependency
"API Producer"->"API Producer": define contracts
"API Producer"->"Build": run build
"Build"->"SCC Plugin": generate \ntests, stubs and stubs \nartifact (e.g. stubs-jar)
"Build"->"Stub Storage": upload contracts \nand stubs and the project arifact
"Build"->"API Producer": Build successful
"API Consumer"->"API Consumer": add SCC Stub Runner \ndependency
"API Consumer"->"API Consumer": write a SCC Stub Runner \nbased contract test
"SCC Stub Runner"->"Stub Storage": test asks for [API Producer] stubs
"Stub Storage"->"SCC Stub Runner": fetch the [API Producer] stubs
"SCC Stub Runner"->"SCC Stub Runner": run in memory\n HTTP server stubs
"API Consumer"->"SCC Stub Runner": send a request \nto the HTTP server stub
"SCC Stub Runner"->"API Consumer": communication is correct

On the Producer Side

要开始使用 Spring Cloud Contract,您可以向构建文件添加 Spring Cloud Contract Verifier 依赖项和插件,如下例所示:

To start working with Spring Cloud Contract, you can add the Spring Cloud Contract Verifier dependency and plugin to your build file, as the following example shows:

Unresolved directive in first-application.adoc - include::{samples_path}/standalone/dsl/http-server/pom.xml[]

以下清单显示了如何添加插件,该插件应位于文件的 build/plugin 部分:

The following listing shows how to add the plugin, which should go in the build/plugins portion of the file:

<plugin>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-contract-maven-plugin</artifactId>
	<version>${spring-cloud-contract.version}</version>
	<extensions>true</extensions>
</plugin>

最简单的入门方法是转到 the Spring Initializr 并将 “Web” 和 “Contract Verifier” 添加为依赖项。这样做会拉入前面提到的依赖项以及 pom.xml 文件中所需的一切(除了设置基础测试类,我们稍后在本节中介绍这一点)。下图显示了在 the Spring Initializr 中使用的设置:

The easiest way to get started is to go to the Spring Initializr and add “Web” and “Contract Verifier” as dependencies. Doing so pulls in the previously mentioned dependencies and everything else you need in the pom.xml file (except for setting the base test class, which we cover later in this section). The following image shows the settings to use in the Spring Initializr: image::start_spring_io_dependencies.png[]

现在,您可以将使用 Groovy DSL 或 YAML 编写的 REST/ 消息传递合约的文件添加到合约目录,该目录由 contractsDslDir 属性设置。默认情况下,它是 $rootDir/src/test/resources/contracts。请注意,文件名无关紧要。您可以使用您喜欢的任何命名方案在此目录中组织合约。

Now you can add files with REST/ messaging contracts expressed in either Groovy DSL or YAML to the contracts directory, which is set by the contractsDslDir property. By default, it is $rootDir/src/test/resources/contracts. Note that the file name does not matter. You can organize your contracts within this directory with whatever naming scheme you like.

对于 HTTP 存根,合约定义了应为给定请求返回哪种类型的响应(考虑 HTTP 方法、URL、标头、状态代码等)。以下示例显示了 Groovy 和 YAML 中的 HTTP 存根合约:

For the HTTP stubs, a contract defines what kind of response should be returned for a given request (taking into account the HTTP methods, URLs, headers, status codes, and so on). The following example shows an HTTP stub contract in both Groovy and YAML:

  • groovy

  • yaml

package contracts

org.springframework.cloud.contract.spec.Contract.make {
	request {
		method 'PUT'
		url '/fraudcheck'
		body([
			   "client.id": $(regex('[0-9]{10}')),
			   loanAmount: 99999
		])
		headers {
			contentType('application/json')
		}
	}
	response {
		status OK()
		body([
			   fraudCheckStatus: "FRAUD",
			   "rejection.reason": "Amount too high"
		])
		headers {
			contentType('application/json')
		}
	}
}
request:
  method: PUT
  url: /fraudcheck
  body:
    "client.id": 1234567890
    loanAmount: 99999
  headers:
    Content-Type: application/json
  matchers:
    body:
      - path: $.['client.id']
        type: by_regex
        value: "[0-9]{10}"
response:
  status: 200
  body:
    fraudCheckStatus: "FRAUD"
    "rejection.reason": "Amount too high"
  headers:
    Content-Type: application/json;charset=UTF-8

如果您需要使用消息传递,您可以定义:

If you need to use messaging, you can define:

  • The input and output messages (taking into account from where it was sent, the message body, and the header).

  • The methods that should be called after the message is received.

  • The methods that, when called, should trigger a message.

以下示例显示了 Camel 消息传递合约:

The following example shows a Camel messaging contract:

  • groovy

  • yaml

Unresolved directive in first-application.adoc - include::{verifier_root_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilderSpec.groovy[]
Unresolved directive in first-application.adoc - include::{verifier_root_path}/src/test/resources/yml/contract_message_scenario1.yml[]

运行 ./mvnw clean install 会自动生成验证应用程序与添加的合约兼容性的测试。默认情况下,生成的测试在 org.springframework.cloud.contract.verifier.tests. 下。

Running ./mvnw clean install automatically generates tests that verify the application compliance with the added contracts. By default, the generated tests are under org.springframework.cloud.contract.verifier.tests..

生成的测试可能有所不同,具体取决于您在插件中设置的框架和测试类型。

The generated tests may differ, depending on which framework and test type you have set up in your plugin.

在下一个列表中,您可以找到:

In the next listing, you can find:

  • The default test mode for HTTP contracts in MockMvc

  • A JAX-RS client with the JAXRS test mode

  • A WebTestClient-based test (this is particularly recommended while working with Reactive, Web-Flux-based applications) set with the WEBTESTCLIENT test mode

你只需要其中一个测试框架。MockMvc 是默认设置。要使用其他框架之一,请将它的库添加到你的 classpath。

You need only one of these test frameworks. MockMvc is the default. To use one of the other frameworks, add its library to your classpath.

以下列表显示了所有框架的示例:

The following listing shows samples for all frameworks:

  • mockmvc

  • jaxrs

  • webtestclient

@Test
public void validate_shouldMarkClientAsFraud() throws Exception {
    // given:
        MockMvcRequestSpecification request = given()
                .header("Content-Type", "application/vnd.fraud.v1+json")
                .body("{\"client.id\":\"1234567890\",\"loanAmount\":99999}");

    // when:
        ResponseOptions response = given().spec(request)
                .put("/fraudcheck");

    // then:
        assertThat(response.statusCode()).isEqualTo(200);
        assertThat(response.header("Content-Type")).matches("application/vnd.fraud.v1.json.*");
    // and:
        DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
        assertThatJson(parsedJson).field("['fraudCheckStatus']").matches("[A-Z]{5}");
        assertThatJson(parsedJson).field("['rejection.reason']").isEqualTo("Amount too high");
}
public class FooTest {
  WebTarget webTarget;

  @Test
  public void validate_() throws Exception {

    // when:
      Response response = webTarget
              .path("/users")
              .queryParam("limit", "10")
              .queryParam("offset", "20")
              .queryParam("filter", "email")
              .queryParam("sort", "name")
              .queryParam("search", "55")
              .queryParam("age", "99")
              .queryParam("name", "Denis.Stepanov")
              .queryParam("email", "bob@email.com")
              .request()
              .build("GET")
              .invoke();
      String responseAsString = response.readEntity(String.class);

    // then:
      assertThat(response.getStatus()).isEqualTo(200);

    // and:
      DocumentContext parsedJson = JsonPath.parse(responseAsString);
      assertThatJson(parsedJson).field("['property1']").isEqualTo("a");
  }

}
@Test
	public void validate_shouldRejectABeerIfTooYoung() throws Exception {
		// given:
			WebTestClientRequestSpecification request = given()
					.header("Content-Type", "application/json")
					.body("{\"age\":10}");

		// when:
			WebTestClientResponse response = given().spec(request)
					.post("/check");

		// then:
			assertThat(response.statusCode()).isEqualTo(200);
			assertThat(response.header("Content-Type")).matches("application/json.*");
		// and:
			DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
			assertThatJson(parsedJson).field("['status']").isEqualTo("NOT_OK");
	}

由于尚未实现由合同描述的功能,因此测试失败。

As the implementation of the functionalities described by the contracts is not yet present, the tests fail.

要使它们通过,您必须添加处理 HTTP 请求或消息的正确实现。此外,您必须为自动生成的测试向项目添加一个基本测试类。所有自动生成的测试都将扩展此类,该类应包含运行它们所需的所有必要设置信息(例如,RestAssuredMockMvc 控制器设置或消息传递测试设置)。

To make them pass, you must add the correct implementation of handling either HTTP requests or messages. Also, you must add a base test class for auto-generated tests to the project. This class is extended by all the auto-generated tests and should contain all the setup necessary information needed to run them (for example, RestAssuredMockMvc controller setup or messaging test setup).

以下示例来自 pom.xml,展示了如何指定基本测试类:

The following example, from pom.xml, shows how to specify the base test class:

<build>
        <plugins>
            <plugin>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-contract-maven-plugin</artifactId>
                <version>2.1.2.RELEASE</version>
                <extensions>true</extensions>
                <configuration>
                    <baseClassForTests>com.example.contractTest.BaseTestClass</baseClassForTests> 1
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
1 The baseClassForTests element lets you specify your base test class. It must be a child of a configuration element within spring-cloud-contract-maven-plugin.

以下示例显示了一个最小的(但可以正常工作的)基本测试类:

The following example shows a minimal (but functional) base test class:

package com.example.contractTest;

import org.junit.Before;

import io.restassured.module.mockmvc.RestAssuredMockMvc;

public class BaseTestClass {

	@Before
	public void setup() {
		RestAssuredMockMvc.standaloneSetup(new FraudController());
	}
}

这个最小的类确实就是让您的测试正常工作所需的一切。它用作自动生成的测试附加到的起始点。

This minimal class really is all you need to get your tests to work. It serves as a starting place to which the automatically generated tests attach.

现在我们可以继续实现。为此,我们首先需要一个数据类,然后在控制器中使用它。以下列表显示了数据类:

Now we can move on to the implementation. For that, we first need a data class, which we then use in our controller. The following listing shows the data class:

package com.example.Test;

import com.fasterxml.jackson.annotation.JsonProperty;

public class LoanRequest {

	@JsonProperty("client.id")
	private String clientId;

	private Long loanAmount;

	public String getClientId() {
		return clientId;
	}

	public void setClientId(String clientId) {
		this.clientId = clientId;
	}

	public Long getLoanAmount() {
		return loanAmount;
	}

	public void setLoanRequestAmount(Long loanAmount) {
		this.loanAmount = loanAmount;
	}
}

前面的类提供了一个对象,我们可以在其中存储参数。因为协议中的客户端 ID 称为 client.id,所以我们需要使用 @JsonProperty("client.id") 参数将其映射到 clientId 字段。

The preceding class provides an object in which we can store the parameters. Because the client ID in the contract is called client.id, we need to use the @JsonProperty("client.id") parameter to map it to the clientId field.

现在我们可以继续处理控制器,如下所示:

Now we can move along to the controller, which the following listing shows:

package com.example.docTest;

import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class FraudController {

	@PutMapping(value = "/fraudcheck", consumes="application/json", produces="application/json")
	public String check(@RequestBody LoanRequest loanRequest) { 1

		if (loanRequest.getLoanAmount() > 10000) { 2
			return "{fraudCheckStatus: FRAUD, rejection.reason: Amount too high}"; 3
		} else {
			return "{fraudCheckStatus: OK, acceptance.reason: Amount OK}"; 4
		}
	}
}
1 We map the incoming parameters to a LoanRequest object.
2 We check the requested loan amount to see if it is too much.
3 If it is too much, we return the JSON (created with a simple string here) that the test expects.
4 If we had a test to catch when the amount is allowable, we could match it to this output.

FraudController 非常简单。您可以执行更多操作,包括记录、验证客户端 ID 等。

The FraudController is about as simple as things get. You can do much more, including logging, validating the client ID, and so on.

一旦实现和测试基础类就位,测试就会通过,应用程序和存根制品都会在本地 Maven 存储库中构建并安装。如下例所示,有关将存根 jar 安装到本地存储库的信息显示在日志中:

Once the implementation and the test base class are in place, the tests pass, and both the application and the stub artifacts are built and installed in the local Maven repository. Information about installing the stubs jar to the local repository appears in the logs, as the following example shows:

[INFO] --- spring-cloud-contract-maven-plugin:1.0.0.BUILD-SNAPSHOT:generateStubs (default-generateStubs) @ http-server ---
[INFO] Building jar: /some/path/http-server/target/http-server-0.0.1-SNAPSHOT-stubs.jar
[INFO]
[INFO] --- maven-jar-plugin:2.6:jar (default-jar) @ http-server ---
[INFO] Building jar: /some/path/http-server/target/http-server-0.0.1-SNAPSHOT.jar
[INFO]
[INFO] --- spring-boot-maven-plugin:1.5.5.BUILD-SNAPSHOT:repackage (default) @ http-server ---
[INFO]
[INFO] --- maven-install-plugin:2.5.2:install (default-install) @ http-server ---
[INFO] Installing /some/path/http-server/target/http-server-0.0.1-SNAPSHOT.jar to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT.jar
[INFO] Installing /some/path/http-server/pom.xml to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT.pom
[INFO] Installing /some/path/http-server/target/http-server-0.0.1-SNAPSHOT-stubs.jar to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT-stubs.jar

现在您可以合并更改并在在线存储库中发布应用程序和存根制品。

You can now merge the changes and publish both the application and the stub artifacts in an online repository.

On the Consumer Side

您可以在集成测试中使用 Spring Cloud Contract Stub Runner 获得运行 WireMock 实例或模拟实际服务的消息传递路由。

You can use Spring Cloud Contract Stub Runner in the integration tests to get a running WireMock instance or messaging route that simulates the actual service.

首先,请按如下方式向“Spring Cloud Contract Stub Runner”添加依赖项:

To get started, add the dependency to Spring Cloud Contract Stub Runner, as follows:

Unresolved directive in first-application.adoc - include::{samples_path}/standalone/dsl/http-client/pom.xml[]

您可以采用两种方式之一在您的 Maven 存储库中安装 Producer 端存根:

You can get the Producer-side stubs installed in your Maven repository in either of two ways:

  • By checking out the Producer side repository and adding contracts and generating the stubs by running the following commands:[source, bash]

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

这些测试将予以跳过,因为 Producer 端契约实现尚未到位,因此自动生成的契约测试将会失败。

The tests are skipped because the Producer-side contract implementation is not yet in place, so the automatically-generated contract tests fail.

  • By getting existing producer service stubs from a remote repository. To do so, pass the stub artifact IDs and artifact repository URL as Spring Cloud Contract Stub Runner properties, as the following example shows:[source, yaml]

Unresolved directive in first-application.adoc - include::{samples_path}/standalone/dsl/http-client/src/test/resources/application-test-repo.yaml[]

现在,您可以使用 @AutoConfigureStubRunner 为您的测试类添加注释。在注释中,提供 group-idartifact-id,以便 Spring Cloud Contract Stub Runner 为您运行合作者的存根,如下例所示:

Now you can annotate your test class with @AutoConfigureStubRunner. In the annotation, provide the group-id and artifact-id for Spring Cloud Contract Stub Runner to run the collaborators' stubs for you, as the following example shows:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment=WebEnvironment.NONE)
@AutoConfigureStubRunner(ids = {"com.example:http-server-dsl:+:stubs:6565"},
		stubsMode = StubRunnerProperties.StubsMode.LOCAL)
public class LoanApplicationServiceTests {
	. . .
}

在线储存库中下载存根时,请使用 REMOTE stubsMode,而离线工作时则使 LOCAL

Use the REMOTE stubsMode when downloading stubs from an online repository and LOCAL for offline work.

在您的集成测试中,您可以接收由合作者服务预期发出的 HTTP 响应或消息的存根版本。您可以在构建日志中看到类似于以下内容的条目:

In your integration test, you can receive stubbed versions of HTTP responses or messages that are expected to be emitted by the collaborator service. You can see entries similar to the following in the build logs:

2016-07-19 14:22:25.403  INFO 41050 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Desired version is + - will try to resolve the latest version
2016-07-19 14:22:25.438  INFO 41050 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Resolved version is 0.0.1-SNAPSHOT
2016-07-19 14:22:25.439  INFO 41050 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Resolving artifact com.example:http-server:jar:stubs:0.0.1-SNAPSHOT using remote repositories []
2016-07-19 14:22:25.451  INFO 41050 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Resolved artifact com.example:http-server:jar:stubs:0.0.1-SNAPSHOT to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT-stubs.jar
2016-07-19 14:22:25.465  INFO 41050 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Unpacking stub from JAR [URI: file:/path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT-stubs.jar]
2016-07-19 14:22:25.475  INFO 41050 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Unpacked file to [/var/folders/0p/xwq47sq106x1_g3dtv6qfm940000gq/T/contracts100276532569594265]
2016-07-19 14:22:27.737  INFO 41050 --- [           main] o.s.c.c.stubrunner.StubRunnerExecutor    : All stubs are now running RunningStubs [namesAndPorts={com.example:http-server:0.0.1-SNAPSHOT:stubs=8080}]