快速开始

1. 引入 Spring Cloud Contract

Spring Cloud Contract 将测试驱动开发(TDD)提升到了软件架构的层面。它允许您执行消费者驱动和生产者驱动的契约测试。spring-doc.cadn.net.cn

1.1. 历史

在成为 Spring Cloud Contract 之前,该项目曾名为 Accurest。它由 Marcin GrzejszczakJakub Kubrynski(来自 Codearte)创建。spring-doc.cadn.net.cn

The 0.1.0 release took place on 26 Jan 2015 and it became stable with 1.0.0 release on 29 Feb 2016.spring-doc.cadn.net.cn

1.1.1. 为什么需要它?

假设我们有一个由多个微服务组成的系统,如以下图片所示:spring-doc.cadn.net.cn

Microservices Architecture

1.1.2. 测试问题

如果我们想测试前一节图片左上角的应用程序,以确定它能否与其他服务通信,我们可以采取以下两种方法之一:spring-doc.cadn.net.cn

两者各有其优势,但也存在许多劣势。spring-doc.cadn.net.cn

部署所有微服务并执行端到端测试spring-doc.cadn.net.cn

在单元测试和集成测试中模拟其他微服务spring-doc.cadn.net.cn

为了解决上述问题,Spring Cloud Contract 应运而生。其核心思想是,在无需搭建整个微服务生态系统的情况下,为您提供极快的反馈。如果您正在使用存根(stubs),那么您只需启动那些您的应用直接依赖的应用程序即可。下图展示了存根与应用程序之间的关系:spring-doc.cadn.net.cn

Stubbed Services

Spring Cloud Contract 使您能够确信,您所使用的存根(stubs)是由您调用的服务创建的。此外,如果可以使用这些存根,说明它们已在生产者端(producer’s side)经过测试。简而言之,您可以信赖这些存根。spring-doc.cadn.net.cn

1.2. 目的

Spring Cloud Contract 的主要目的包括:spring-doc.cadn.net.cn

  • 为了确保HTTP和消息存根(在开发客户端时使用)能够完全执行实际服务器端实现的功能。spring-doc.cadn.net.cn

  • 为了促进ATDD(基于验收测试的驱动开发)方法和微服务体系结构风格。spring-doc.cadn.net.cn

  • 提供一种方式,以发布合同中的变更,使双方都能立即看到这些变更。spring-doc.cadn.net.cn

  • 生成用于服务器端使用的样板测试代码。spring-doc.cadn.net.cn

默认情况下,Spring Cloud Contract 将 Wiremock 集成作为 HTTP 服务器存根。spring-doc.cadn.net.cn

Spring Cloud Contract 的目的在于 NOT 开始在合同中编写业务功能。假设我们有一个欺诈检查的业务用例。如果一个用户可以有100种不同的欺诈原因,我们会假设你会创建两个合同,一个用于正向情况,另一个用于负向情况。合同测试用于测试应用程序之间的合同,而不是模拟完整行为。

1.3 合同是什么?

作为服务的消费者,我们需要明确我们究竟希望实现什么目标。我们需要制定我们的期望。这就是为什么我们要编写契约。换句话说,契约是关于API或消息通信应如何呈现的协议。考虑以下示例:spring-doc.cadn.net.cn

假设您想要发送一个请求,其中包含客户公司的ID以及他们希望从我们这里借款的金额。您还希望通过PUT方法将它发送到/fraudcheckURL。下面列出的内容展示了使用Groovy和YAML编写的检查客户端是否应被标记为欺诈行为的契约:spring-doc.cadn.net.cn

Groovy
/*
 * Copyright 2013-2020 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

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: /yamlfraudcheck # (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


#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 "/yamlfraudcheck"
#(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 "/yamlfraudcheck"
#(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`

2. 三秒之旅

本简短的指南介绍了如何使用 Spring Cloud Contract。它包含以下主题:spring-doc.cadn.net.cn

您可以在此处找到更详细的指南。spring-doc.cadn.net.cn

以下UML图显示了Spring Cloud Contract中各部分之间的关系:spring-doc.cadn.net.cn

getting started three second

2.1. 在生产者端

要开始使用 Spring Cloud Contract,您可以将使用 Groovy DSL 或 YAML 编写的 REST 或消息契约文件添加到 contracts 目录中,该目录由 contractsDslDir 属性指定。默认情况下,其值为 $rootDir/src/test/resources/contractsspring-doc.cadn.net.cn

然后,您可以在构建文件中添加 Spring Cloud Contract Verifier 依赖项和插件,如下所示的示例:spring-doc.cadn.net.cn

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-contract-verifier</artifactId>
    <scope>test</scope>
</dependency>

以下列表显示了如何添加插件,该插件应放置在文件的 build/plugins 部分中:spring-doc.cadn.net.cn

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

运行 ./mvnw clean install 会自动生成测试用例,以验证应用程序对新增契约的合规性。默认情况下,测试用例将生成于 org.springframework.cloud.contract.verifier.tests. 目录下。spring-doc.cadn.net.cn

由于尚未实现由契约所描述的功能,测试失败。spring-doc.cadn.net.cn

要使它们通过,您必须添加正确的 HTTP 请求或消息处理实现。此外,您还必须将一个基础测试类添加到项目中,以便自动生成的测试使用该类。所有自动生成的测试都继承自该类,它应包含运行这些测试所需的所有设置信息(例如 RestAssuredMockMvc 控制器设置或消息测试设置)。spring-doc.cadn.net.cn

以下示例来自 pom.xml,展示了如何指定基础测试类:spring-doc.cadn.net.cn

<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 元素让您能够指定基础测试类。它必须是 configuration 元素的子元素,而该 configuration 元素又需位于 spring-cloud-contract-maven-plugin 元素内部。

一旦实现和测试基类到位,测试即可通过,且应用程序和存根工件均已在本地 Maven 仓库中构建并安装。现在您可以合并这些更改,并将应用程序和存根工件发布到在线仓库中。spring-doc.cadn.net.cn

2.2. 在消费者端

您可以在集成测试中使用 Spring Cloud Contract Stub Runner 来获取一个正在运行的 WireMock 实例或消息路由,以模拟实际的服务。spring-doc.cadn.net.cn

为此,请将依赖项添加到 Spring Cloud Contract Stub Runner,如下例所示:spring-doc.cadn.net.cn

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
    <scope>test</scope>
</dependency>

您可以通过以下两种方式之一,将生产者端存根安装到您的Maven仓库中:spring-doc.cadn.net.cn

  • 通过检出生产者端仓库,并添加契约(contracts)并生成存根(stubs),可运行以下命令:spring-doc.cadn.net.cn

    $ cd local-http-server-repo
    $ ./mvnw clean install -DskipTests
    测试被跳过,因为生产者端契约实现尚未到位,因此自动生成的契约测试失败。
  • 通过从远程仓库获取已存在的生产者服务存根。为此,可将存根工件ID和工件仓库URL作为Spring Cloud Contract Stub Runner属性传入,如下例所示:spring-doc.cadn.net.cn

    stubrunner:
      ids: 'com.example:http-server-dsl:+:stubs:8080'
      repositoryRoot: https://repo.spring.io/libs-snapshot

现在,您可以使用 @AutoConfigureStubRunner 注解您的测试类。在注解中,为 Spring Cloud Contract Stub Runner 提供 group-idartifact-id 的值,以便为您运行协作对象的存根(stubs),如下例所示:spring-doc.cadn.net.cn

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment=WebEnvironment.NONE)
@AutoConfigureStubRunner(ids = {"com.example:http-server-dsl:+:stubs:6565"},
        stubsMode = StubRunnerProperties.StubsMode.LOCAL)
public class LoanApplicationServiceTests {
在从在线仓库下载存根时使用 REMOTEstubsMode,而在离线工作时使用 LOCAL

现在,在您的集成测试中,您可以接收来自协作服务的存根版本的 HTTP 响应或预期由协作服务发出的消息。spring-doc.cadn.net.cn

3. 开发您的第一个基于 Spring Cloud Contract 的应用程序

本简短指南介绍了如何使用 Spring Cloud Contract。其内容包括以下主题:spring-doc.cadn.net.cn

您可以在这里找到更简短的指南。spring-doc.cadn.net.cn

为了便于本示例说明,Stub Storage 代表 Nexus/Artifactory。spring-doc.cadn.net.cn

以下UML图显示了Spring Cloud Contract各部分之间的关系:spring-doc.cadn.net.cn

Getting started first application

3.1. 在生产者端

要开始使用Spring Cloud Contract,可以向构建文件添加Spring Cloud Contract Verifier依赖项和插件,如下例所示:spring-doc.cadn.net.cn

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-contract-verifier</artifactId>
    <scope>test</scope>
</dependency>

以下列表显示了如何添加插件,该插件应放置在文件的 build/plugins 部分中:spring-doc.cadn.net.cn

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

最简单的入门方式是前往 Spring Initializr,并添加“Web”和“Contract Verifier”作为依赖项。这样做会引入前述所提及的依赖项以及 pom.xml 文件中所需的其他所有内容(除非设置基础测试类,这部分我们将在本节稍后介绍)。下图展示了在 Spring Initializr 中应使用的配置:spring-doc.cadn.net.cn

Spring Initializr with Web and Contract Verifier

现在,您可以将使用 REST/ 消息契约(以 Groovy DSL 或 YAML 格式表达)的文件添加到 contracts 目录中,该目录由 contractsDslDir 属性设置。默认情况下,它为 $rootDir/src/test/resources/contracts
请注意,文件名并不重要。您可按任意命名方案在该目录内组织您的契约。spring-doc.cadn.net.cn

对于 HTTP 模拟(stubs),契约定义了在给定请求条件下应返回何种响应(考虑 HTTP 方法、URL、头信息、状态码等)。以下示例展示了以 Groovy 和 YAML 格式编写的 HTTP 模拟契约:spring-doc.cadn.net.cn

Groovy
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
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

如果您需要使用消息传递功能,可以定义:spring-doc.cadn.net.cn

以下示例显示了一个 Camel 消息契约:spring-doc.cadn.net.cn

Groovy
def contractDsl = Contract.make {
    name "foo"
    label 'some_label'
    input {
        messageFrom('jms:delete')
        messageBody([
                bookName: 'foo'
        ])
        messageHeaders {
            header('sample', 'header')
        }
        assertThat('bookWasDeleted()')
    }
}
YAML
label: some_label
input:
  messageFrom: jms:delete
  messageBody:
    bookName: 'foo'
  messageHeaders:
    sample: header
  assertThat: bookWasDeleted()

运行 ./mvnw clean install 会自动生成测试用例,以验证应用程序对新增契约的合规性。默认情况下,生成的测试位于 org.springframework.cloud.contract.verifier.tests.spring-doc.cadn.net.cn

生成的测试可能因您在插件中设置的框架和测试类型不同而有所差异。spring-doc.cadn.net.cn

在下一个列表中,您可以找到:spring-doc.cadn.net.cn

您只需选择其中一种测试框架。MockMvc 是默认选项。若要使用其他框架,请将其库添加到您的类路径中。

以下列表展示了所有框架的示例:spring-doc.cadn.net.cn

MockMvc
@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");
}
jaxrs
@SuppressWarnings("rawtypes")
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", "[email protected]")
              .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");
  }

}
WebTestClient
@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");
    }
Spock
given:
     ContractVerifierMessage inputMessage = contractVerifierMessaging.create(
        \'\'\'{"bookName":"foo"}\'\'\',
        ['sample': 'header']
    )

when:
     contractVerifierMessaging.send(inputMessage, 'jms:delete')

then:
     noExceptionThrown()
     bookWasDeleted()

由于尚未实现由契约所描述的功能,测试失败。spring-doc.cadn.net.cn

要使它们通过,您必须添加正确的实现以处理HTTP请求或消息。此外,您还必须将一个基础测试类添加到项目中,该类供自动生成的测试使用。所有自动生成的测试都继承自该类,其中应包含运行这些测试所需的所有设置信息(例如,RestAssuredMockMvc 控制器设置或消息测试设置)。spring-doc.cadn.net.cn

以下示例来自 pom.xml,展示了如何指定基础测试类:spring-doc.cadn.net.cn

<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 元素让您能够指定基础测试类。它必须是 configuration 元素的子元素,而该 configuration 元素又需位于 spring-cloud-contract-maven-plugin 元素内部。

以下示例显示了一个最小(但功能完整)的基础测试类:spring-doc.cadn.net.cn

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());
    }
}

这个最简化的类实际上就是您所需的一切,可使您的测试正常运行。它作为起点,自动生成功能测试将附加于此。spring-doc.cadn.net.cn

现在我们可以继续进行实现。为此,我们首先需要一个数据类,然后在我们的控制器中使用它。以下列表显示了该数据类:spring-doc.cadn.net.cn

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 字段。spring-doc.cadn.net.cn

现在我们可以继续讨论控制器,如下列表所示:spring-doc.cadn.net.cn

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 我们映射传入参数到一个LoanRequest对象。
2 我们检查所需的贷款金额,以确保不会过多。
3 如果太多了,我们返回JSON(在这里用一个简单字符串创建)...
4 如果我们有一个测试来捕捉当金额是允许的时候,我们可以匹配这个输出。

the FraudController is about as simple as things get. you can do much more, including logging, validating the client id, and so on.spring-doc.cadn.net.cn

实现类和测试基类就位后,测试通过,并且应用程序和桩工件都已构建并安装到本地Maven存储库中。有关将桩jar文件安装到本地存储库的信息,请参见日志输出,如下例所示:spring-doc.cadn.net.cn

[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

现在您可以合并这些更改并在线发布应用和存根 artifact。spring-doc.cadn.net.cn

3.2. 在消费者端

你可以使用 Spring Cloud Contract Stub Runner 在集成测试中获取一个正在运行的 WireMock 实例或消息路由,以模拟实际服务。spring-doc.cadn.net.cn

要开始,请在 Spring Cloud Contract Stub Runner 中添加依赖,如下所示:spring-doc.cadn.net.cn

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
    <scope>test</scope>
</dependency>

您可以通过以下两种方式之一,将生产者端存根安装到您的Maven仓库中:spring-doc.cadn.net.cn

  • By checking out the Producer side repository and adding contracts and generating the stubs by running the following commands:spring-doc.cadn.net.cn

    $ cd local-http-server-repo
    $ ./mvnw clean install -DskipTests
    The tests are skipped because the Producer-side contract implementation is not yet in place, so the automatically-generated contract tests fail.
  • 从远程存储库获取已存在的生产者服务存根。为此,作为Spring Cloud Contract Stub Runner属性传递存根工件ID和工件存储库URL,如下例所示:spring-doc.cadn.net.cn

    stubrunner:
      ids: 'com.example:http-server-dsl:+:stubs:8080'
      repositoryRoot: https://repo.spring.io/libs-snapshot

现在您可以使用 @AutoConfigureStubRunner 在测试类上进行注解。在注解中, 为 group-idartifact-id 提供 Spring Cloud Contract Stub Runner,以便为您自动运行合作者的存档,如下面的例子所示:spring-doc.cadn.net.cn

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment=WebEnvironment.NONE)
@AutoConfigureStubRunner(ids = {"com.example:http-server-dsl:+:stubs:6565"},
        stubsMode = StubRunnerProperties.StubsMode.LOCAL)
public class LoanApplicationServiceTests {
在从在线仓库下载存根时使用 REMOTEstubsMode,而在离线工作时使用 LOCAL

在你的集成测试中,您可以接收来自合作者服务预期发出的HTTP响应或消息的存根版本。您可以在构建日志中看到类似的条目:spring-doc.cadn.net.cn

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}]

4. 使用生产者端合同的消费者驱动契约(CDC)逐步指南

考虑一个欺诈检测和贷款发放流程的例子。业务场景是,我们希望向人们发放贷款,但不希望他们从我们这里窃取资金。当前系统的实现方式是向所有人发放贷款。spring-doc.cadn.net.cn

假设 Loan IssuanceFraud Detection 服务器的客户端。在当前冲刺中,我们必须开发一项新功能:如果客户申请的贷款金额过高,我们将该客户标记为欺诈者。spring-doc.cadn.net.cn

服务器端代码可在这里找到,客户端代码可在这里获取。spring-doc.cadn.net.cn

在这种情况下,生产商拥有合同。在物理上,所有合同都在生产商的仓库中。

4.1. 技术说明

如果您使用快照版本、里程碑版本或发布候选版本,则需要将以下部分添加到您的构建中:spring-doc.cadn.net.cn

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>
Gradle
repositories {
    mavenCentral()
    mavenLocal()
    maven { url "https://repo.spring.io/snapshot" }
    maven { url "https://repo.spring.io/milestone" }
    maven { url "https://repo.spring.io/release" }
}

为简化起见,我们使用以下缩写:spring-doc.cadn.net.cn

4.2. 消费者端(贷款发放)

作为贷款发放服务(欺诈检测服务器的消费者)的开发者,您可能需要执行以下步骤:spring-doc.cadn.net.cn

  1. 通过为您的功能编写测试来开始进行TDD(测试驱动开发)。spring-doc.cadn.net.cn

  2. 编写缺失的实现。spring-doc.cadn.net.cn

  3. 本地克隆欺诈检测服务仓库。spring-doc.cadn.net.cn

  4. 在欺诈检测服务的代码仓库中本地定义合约。spring-doc.cadn.net.cn

  5. 添加Spring Cloud Contract(SCC)插件。spring-doc.cadn.net.cn

  6. 运行集成测试。spring-doc.cadn.net.cn

  7. 提交Pull Request。spring-doc.cadn.net.cn

  8. 创建一个初始实现。spring-doc.cadn.net.cn

  9. 完成拉取请求。spring-doc.cadn.net.cn

  10. 编写缺失的实现。spring-doc.cadn.net.cn

  11. 部署您的应用程序。spring-doc.cadn.net.cn

  12. 在线工作。spring-doc.cadn.net.cn

我们先来看贷款发放流程,下图显示了该流程的 UML 图:spring-doc.cadn.net.cn

getting started cdc client

4.2.1. 通过为您的功能编写测试来开始TDD

下面列出了我们可能用来检查贷款金额是否过大的测试代码:spring-doc.cadn.net.cn

@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");
}

假设你已经编写了你新功能的测试。如果收到一笔大金额的贷款申请,系统应该拒绝该贷款申请并附带一些描述。spring-doc.cadn.net.cn

4.2.2. 编写缺失的实现

到某个时间点,您需要向欺诈检测服务发送请求。假设需要发送包含客户端 ID 和客户想要借款金额的请求。您要使用PUT方法将它发送到/fraudcheck URL。为此,您可以使用类似的代码:spring-doc.cadn.net.cn

ResponseEntity<FraudServiceResponse> response = restTemplate.exchange(
        "http://localhost:" + port + "/fraudcheck", HttpMethod.PUT,
        new HttpEntity<>(request, httpHeaders), FraudServiceResponse.class);

为了简化,欺诈检测服务的端口设置为 8080,并且应用运行在 8090 上。spring-doc.cadn.net.cn

如果您在此处开始测试,将会失败,因为端口8080上没有任何服务当前正在运行。

4.2.3. 将欺诈检测服务仓库本地克隆

你可以先从使用服务端契约开始。为此,必须首先 克隆它,通过运行以下命令:spring-doc.cadn.net.cn

$ git clone https://your-git-server.com/server-side.git local-http-server-repo

4.2.4. 在欺诈检测服务的存储库中本地定义合约

作为消费者,您需要明确您想要实现什么目标。您需要提出您的期望。为此,您需要写下以下合同:spring-doc.cadn.net.cn

将合同放在src/test/resources/contracts/fraud文件夹中。fraud文件夹很重要,因为生产者的测试基础类名引用了该文件夹。

以下示例展示了我们的契约,包括Groovy和YAML两种形式:spring-doc.cadn.net.cn

Groovy
/*
 * Copyright 2013-2020 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

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: /yamlfraudcheck # (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


#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 "/yamlfraudcheck"
#(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 "/yamlfraudcheck"
#(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`

YML 合同非常简单明了。但是,当你查看使用静态类型 Groovy DSL 编写的合同内容时,你可能会对 value(client(…​), server(…​)) 部分感到疑惑。通过这种表示法,Spring Cloud Contract 允许您定义 JSON 块、URL 或其他结构中动态的部分。对于标识符或时间戳等值,无需硬编码特定值。相反,您希望允许一些不同范围的值。为了启用这些值的范围,您可以为消费者端设置匹配这些值的正则表达式。您可以使用 map 表示法或带有插值功能的字符串来提供正文。我们强烈推荐使用 map 表示法。spring-doc.cadn.net.cn

在设置合同之前,您必须理解映射符号。请参阅关于JSON的Groovy文档

之前展示的合同是双方之间的协议,规定了:spring-doc.cadn.net.cn

一旦你准备好在集成测试中实践检查API,就需要在本地安装存档。spring-doc.cadn.net.cn

4.2.5. 添加 Spring Cloud Contract Verifier 插件

我们可以在Maven或Gradle中添加插件。在本示例中,我们将展示如何添加Maven插件。
首先,如下面的示例所示,我们需要添加Spring Cloud Contract BOM:spring-doc.cadn.net.cn

<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 插件,如下示例所示:spring-doc.cadn.net.cn

            <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>
<!--                    <convertToYaml>true</convertToYaml>-->
                </configuration>
                <!-- if additional dependencies are needed e.g. for Pact -->
                <dependencies>
                    <dependency>
                        <groupId>org.springframework.cloud</groupId>
                        <artifactId>spring-cloud-contract-pact</artifactId>
                        <version>${spring-cloud-contract.version}</version>
                    </dependency>
                </dependencies>
            </plugin>

自从添加了插件,您就获得了提供的合同提供的Spring Cloud Contract Verifier个功能:spring-doc.cadn.net.cn

您不希望生成测试,因为作为消费者,您只想使用存根。你需要跳过测试的生成和执行。为此,请运行以下命令:spring-doc.cadn.net.cn

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

一旦你运行这些命令,你就会在日志中看到如下内容:spring-doc.cadn.net.cn

[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

重要的是:spring-doc.cadn.net.cn

[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已安装在本地存储库中。spring-doc.cadn.net.cn

4.2.6. 运行集成测试

为了从 Spring Cloud Contract Stub Runner 功能中获得自动存档下载的好处,您必须在消费方侧项目(0)中执行以下操作:Loan Application service:spring-doc.cadn.net.cn

  1. Add the Spring Cloud Contract BOM, as follows:spring-doc.cadn.net.cn

    <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>
  2. Add the dependency to Spring Cloud Contract Stub Runner, as follows:spring-doc.cadn.net.cn

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
        <scope>test</scope>
    </dependency>
  3. 使用 @AutoConfigureStubRunner 注解您的测试类。在注解中,提供group-idartifact-id,以便 Stub Runner 下载您合作者的桩文件。
    (可选步骤) 因为您正在离线处理合作者,因此也可以提供离线工作开关(StubRunnerProperties.StubsMode.LOCAL)。spring-doc.cadn.net.cn

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

现在,当你运行你的测试时,你将在日志中看到如下输出:spring-doc.cadn.net.cn

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}]

This output means that Stub Runner has found your stubs and started a server for your application with a group ID of com.example and an artifact ID of http-server with version 0.0.1-SNAPSHOT of the stubs and with the stubs classifier on port 8080.spring-doc.cadn.net.cn

4.2.7. 提交拉取请求

您到目前为止所做的就是一个迭代过程。您可以围绕契约进行尝试,将其安装在本地,并在消费者一侧进行工作,直到契约按照您的期望工作。spring-doc.cadn.net.cn

一旦对结果满意并且测试通过,就可以向服务器端提交pull请求。目前,消费端的工作已经完成。spring-doc.cadn.net.cn

4.3. 生产者端(欺诈检测服务器)

作为欺诈检测服务器(贷款发行服务的服务器)的开发者,您可能需要执行以下操作spring-doc.cadn.net.cn

以下UML图展示了欺诈检测流程:spring-doc.cadn.net.cn

getting started cdc server

4.3.1. 接管拉取请求

作为提醒,下表显示了初始实现。spring-doc.cadn.net.cn

@RequestMapping(value = "/fraudcheck", method = PUT)
public FraudCheckResult fraudCheck(@RequestBody FraudCheck fraudCheck) {
return new FraudCheckResult(FraudCheckStatus.OK, NO_REASON);
}

然后,您可以运行以下命令:spring-doc.cadn.net.cn

$ git checkout -b contract-change-pr master
$ git pull https://your-git-server.com/server-side-fork.git contract-change-pr

您必须添加自动生成测试所需的依赖项,如下所示:spring-doc.cadn.net.cn

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-contract-verifier</artifactId>
    <scope>test</scope>
</dependency>

在Maven插件的配置中,必须传递packageWithBaseClasses属性,如下:spring-doc.cadn.net.cn

            <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>
<!--                    <convertToYaml>true</convertToYaml>-->
                </configuration>
                <!-- if additional dependencies are needed e.g. for Pact -->
                <dependencies>
                    <dependency>
                        <groupId>org.springframework.cloud</groupId>
                        <artifactId>spring-cloud-contract-pact</artifactId>
                        <version>${spring-cloud-contract.version}</version>
                    </dependency>
                </dependencies>
            </plugin>
本示例通过设置属性packageWithBaseClasses来使用“基于约定”的命名。这样做意味着最后两个包组合起来构成了基础测试类的名称。在我们的例子中,合同被放在了src/test/resources/contracts/fraud文件夹下。由于您从contracts文件夹开始没有两个包,只需选择一个,应该是fraud。添加后缀Base并首字母大写fraud。这就为您提供了FraudBase测试类名。

All the generated tests extend that class. Over there, you can set up your Spring Context or whatever is necessary. In this case, you should use Rest Assured MVC to start the server side FraudDetectionController. The following listing shows the FraudBase class:spring-doc.cadn.net.cn

/*
 * Copyright 2013-2020 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.example.fraud;

import io.restassured.module.mockmvc.RestAssuredMockMvc;
import org.junit.Before;

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;
    }

}

现在,如果您运行 0,则会得到如下输出:spring-doc.cadn.net.cn

Results :

Tests in error:
  ContractVerifierTest.validate_shouldMarkClientAsFraud:32 » IllegalState Parsed...

此错误出现的原因是您有一个新的合约,从该合约生成了一个测试并且由于您尚未实现该功能而失败了。自动生成的测试方法如下:spring-doc.cadn.net.cn

@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,您可以看到所有在producer()块中出现的Contract中的producer()部分都已注入到测试中。如果使用YAML,则同样的规则适用于response中的matchers部分。spring-doc.cadn.net.cn

请注意,在生产者端,您也在进行TDD。期望以测试的形式表达。该测试将请求发送到我们自己的应用程序,并定义URL、头信息和正文。它还期望响应中完全确定的值。换句话说,你有red部分的redgreenrefactor。是时候将red转换为green了。spring-doc.cadn.net.cn

4.3.2. 编写缺失的实现

因为您知道预期的输入和输出,所以可以按照如下方式编写缺失的实现:spring-doc.cadn.net.cn

@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中运行这些测试。spring-doc.cadn.net.cn

4.3.3. 部署您的应用程序

完成工作后,您可以部署所做的更改。为此,您必须首先通过运行以下命令合并分支:spring-doc.cadn.net.cn

$ git checkout master
$ git merge --no-ff contract-change-pr
$ git push origin master

您的持续集成流程可能会运行类似于 ./mvnw clean deploy 的命令,该命令会同时发布应用程序和桩存档。spring-doc.cadn.net.cn

4.4. 消费者端(贷款发放),最后一步

作为贷款发放服务(欺诈检测服务器的消费者)的开发者,我需要:spring-doc.cadn.net.cn

下面的UML图展示了该过程的最终状态:spring-doc.cadn.net.cn

getting started cdc client final

4.4.1. 将分支合并到主干

以下命令展示了一种使用 Git 将分支合并到 master 的方式:spring-doc.cadn.net.cn

$ git checkout master
$ git merge --no-ff contract-change-pr

4.4.2. 在线工作

现在,您可以为Spring Cloud Contract Stub Runner禁用离线工作,并指定存放开桩的仓库位置。目前,服务器端的开桩会自动从Nexus/Artifactory下载。可以将stubsMode的值设置为REMOTE。以下代码通过更改属性展示了实现相同功能的方法:spring-doc.cadn.net.cn

stubrunner:
  ids: 'com.example:http-server-dsl:+:stubs:8080'
  repositoryRoot: https://repo.spring.io/libs-snapshot

教程到此结束。spring-doc.cadn.net.cn

5. 后续步骤

希望本节提供了一些 Spring Cloud Contract 的基础知识,并帮助您开始编写自己的应用程序。如果您是任务导向型开发者,可以跳到spring.io查看一些解决特定“我如何用Spring做某事?”问题的入门指南。我们还提供了针对 Spring Cloud Contract 的“操作指南”参考文档。spring-doc.cadn.net.cn

否则,下一步是阅读使用Spring Cloud Contract。如果你非常急切,也可以直接跳到Spring Cloud Contract特性部分。spring-doc.cadn.net.cn

此外,您还可以查看以下视频:spring-doc.cadn.net.cn

  • “消费者驱动契约与您的微服务架构”(作者:Olga Maciaszek-Sharma 和 Marcin Grzejszczak)spring-doc.cadn.net.cn

您可以在 samples 中找到默认项目示例。spring-doc.cadn.net.cn

您可以在 这里 找到 Spring Cloud Contract 工作坊。spring-doc.cadn.net.cn