開發您的第一個基於 Spring Cloud Contract 的應用程式

本簡短導覽介紹瞭如何使用 Spring Cloud Contract。它包含以下主題

您可以在 此處 找到更簡短的導覽。

在本示例中,Stub Storage 是 Nexus/Artifactory。

以下 UML 圖顯示了 Spring Cloud Contract 各部分之間的關係

getting-started-three-second

生產者端

要開始使用 Spring Cloud Contract,您可以將 Spring Cloud Contract Verifier 依賴和外掛新增到您的構建檔案,如下例所示

以下列表顯示瞭如何新增外掛,該外掛應位於檔案的 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>

最簡單的入門方法是訪問 Spring Initializr 並新增“Web”和“Contract Verifier”作為依賴。這樣做會引入前面提到的依賴以及您在 pom.xml 檔案(除了設定基礎測試類,這部分將在後面介紹)中需要的其他所有內容。下圖顯示了在 Spring Initializr 中使用的設定

Spring Initializr with Web and Contract Verifier

現在,您可以將使用 Groovy DSL 或 YAML 表達的 REST/ 訊息傳遞契約檔案新增到 contracts 目錄中,該目錄由 contractsDslDir 屬性設定。預設情況下,它是 $rootDir/src/test/resources/contracts。請注意,檔名無關緊要。您可以在此目錄中按照您喜歡的任何命名方案組織您的契約。

對於 HTTP Stubs,契約定義了對於給定請求應返回什麼樣的響應(考慮到 HTTP 方法、URL、頭部、狀態碼等)。以下示例展示了 Groovy 和 YAML 格式的 HTTP Stub 契約

groovy
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

如果您需要使用訊息傳遞,可以定義

  • 輸入和輸出訊息(考慮傳送位置、訊息體和頭部)。

  • 收到訊息後應呼叫的方法。

  • 呼叫後應觸發訊息的方法。

以下示例展示了一個 Camel 訊息傳遞契約

groovy
def contractDsl = Contract.make {
	name "foo"
	label 'some_label'
	input {
		triggeredBy('bookReturnedTriggered()')
	}
	outputMessage {
		sentTo('activemq:output')
		body('''{ "bookName" : "foo" }''')
		headers {
			header('BOOK-NAME', 'foo')
			messagingContentType(applicationJson())
		}
	}
}
yaml
label: some_label
input:
  triggeredBy: bookReturnedTriggered
outputMessage:
  sentTo: activemq:output
  body:
    bookName: foo
  headers:
    BOOK-NAME: foo
    contentType: application/json

執行 ./mvnw clean install 會自動生成測試來驗證應用是否符合新增的契約。預設情況下,生成的測試位於 org.springframework.cloud.contract.verifier.tests. 包下。

生成的測試可能有所不同,具體取決於您在外掛中設定的框架和測試型別。

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

  • MockMvc 中用於 HTTP 契約的預設測試模式

  • 一個使用 JAXRS 測試模式的 JAX-RS 客戶端

  • 一個基於 WebTestClient 的測試(在使用響應式、基於 Web-Flux 的應用時特別推薦),設定了 WEBTESTCLIENT 測試模式

您只需要這些測試框架中的一個。MockMvc 是預設的。要使用其他框架之一,請將其庫新增到您的類路徑中。

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

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

由於契約描述的功能尚未實現,測試將會失敗。

為了讓它們透過,您必須新增處理 HTTP 請求或訊息的正確實現。此外,您必須為專案中自動生成的測試新增一個基礎測試類。所有自動生成的測試都將繼承此基類,並且它應包含執行這些測試所需的所有必要設定資訊(例如,RestAssuredMockMvc 控制器設定或訊息傳遞測試設定)。

以下示例,來自 pom.xml 檔案,展示瞭如何指定基礎測試類

<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 baseClassForTests 元素允許您指定您的基礎測試類。它必須是 spring-cloud-contract-maven-pluginconfiguration 元素的子元素。

以下示例展示了一個最小(但功能齊全)的基礎測試類

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

這個最小類確實是讓您的測試工作所需的一切。它作為一個起點,自動生成的測試將附加到此處。

現在我們可以繼續實現。為此,我們首先需要一個數據類,稍後將在我們的控制器中使用它。以下列表展示了該資料類

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 欄位。

現在我們可以轉到控制器,以下列表展示了它

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 如果我們有一個測試來捕獲金額允許的情況,我們可以將其與此輸出匹配。

FraudController 非常簡單。您可以做更多事情,包括日誌記錄、驗證客戶端 ID 等等。

一旦實現和基礎測試類到位,測試將透過,並且應用程式和 Stub Artifact 都將在本地 Maven 倉庫中構建和安裝。將 stubs jar 安裝到本地倉庫的資訊會出現在日誌中,如下例所示

[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

您現在可以合併更改並將應用程式和 Stub Artifact 釋出到線上倉庫。

消費方端

您可以在整合測試中使用 Spring Cloud Contract Stub Runner 來獲取執行中的 WireMock 例項或模擬實際服務的訊息傳遞路由。

要開始使用,請新增 Spring Cloud Contract Stub Runner 依賴,如下所示

您可以透過以下兩種方式之一將生產者端的 Stubs 安裝到您的 Maven 倉庫

  • 透過檢出生產者端倉庫並新增契約,然後執行以下命令生成 Stubs

    $ cd local-http-server-repo
    $ ./mvnw clean install -DskipTests
    測試被跳過,因為生產者端的契約實現尚未到位,因此自動生成的契約測試會失敗。
  • 從遠端倉庫獲取現有的生產者服務 Stubs。為此,將 Stub Artifact ID 和 Artifact 倉庫 URL 作為 Spring Cloud Contract Stub Runner 屬性傳遞,如下例所示

現在您可以使用 @AutoConfigureStubRunner 註解您的測試類。在註解中,提供 group-idartifact-id,以便讓 Spring Cloud Contract Stub Runner 為您執行協作者的 Stubs,如下例所示

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment=WebEnvironment.NONE)
@AutoConfigureStubRunner(ids = {"com.example:http-server-dsl:+:stubs:6565"},
		stubsMode = StubRunnerProperties.StubsMode.LOCAL)
public class LoanApplicationServiceTests {
	. . .
}
從線上倉庫下載 Stubs 時使用 REMOTE stubsMode,離線工作時使用 LOCAL

在您的整合測試中,您可以接收協作者服務預期會發出的 HTTP 響應或訊息的 Stub 版本。您可以在構建日誌中看到類似於以下的條目

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