消費者驅動合同 (CDC) 循序漸進指南:生產方持有合同

考慮一個欺詐檢測和貸款發放流程的例子。業務場景是我們希望向人們發放貸款,但又不希望他們欺詐我們。我們系統當前的實現會向所有人發放貸款。

假設 Loan IssuanceFraud Detection 伺服器的客戶端。在當前衝刺中,我們必須開發一個新功能:如果客戶想借太多錢,我們將其標記為欺詐。

技術說明

  • Fraud Detection 的 artifact-idhttp-server

  • Loan Issuance 的 artifact-idhttp-client

  • 兩者的 group-id 均為 com.example

  • 為了本例的需要,Stub Storage 為 Nexus/Artifactory。

社交說明

  • 客戶端和服務端開發團隊都需要在整個流程中直接溝通並討論變更。

  • CDC 的核心在於溝通。

服務端程式碼位於 Spring Cloud Contract Samples 倉庫的 samples/standalone/dsl/http-server 路徑下,客戶端程式碼位於 Spring Cloud Contract 倉庫的 samples/standalone/dsl/http-client 路徑下。

在本例中,生產方擁有合同。實際上,所有合同都位於生產方的倉庫中。

技術說明

重要提示:所有程式碼均可在 Spring Cloud Contract Samples 倉庫中找到。

為簡單起見,我們使用以下縮寫

  • Loan Issuance (LI): HTTP 客戶端

  • Fraud Detection (FD): HTTP 伺服器

  • SCC: Spring Cloud Contract

消費方 (Loan Issuance)

作為 Loan Issuance 服務(Fraud Detection 伺服器的消費方)的開發者,您可能會執行以下步驟

  1. 透過為您的功能編寫測試來開始 TDD。

  2. 編寫缺失的實現。

  3. 在本地克隆 Fraud Detection 服務倉庫。

  4. 在本地的 fraud detection 服務倉庫中定義合同。

  5. 新增 Spring Cloud Contract (SCC) 外掛。

  6. 執行整合測試。

  7. 提交 Pull Request。

  8. 建立初始實現。

  9. 接管 Pull Request。

  10. 編寫缺失的實現。

  11. 部署您的應用。

  12. 線上工作。

我們從貸款發放流程開始,如下面的 UML 圖所示

getting-started-cdc-client

透過為您的功能編寫測試來開始 TDD

以下清單顯示了我們可能用於檢查貸款金額是否過大的測試

假設您已經為您的新功能編寫了測試。如果收到大額貸款申請,系統應拒絕該貸款申請並給出一些描述。

編寫缺失的實現

在某個時候,您需要向 Fraud Detection 服務傳送請求。假設您需要傳送包含客戶 ID 和客戶希望借款金額的請求。您希望使用 PUT 方法將其傳送到 /fraudcheck URL。為此,您可以使用類似如下的程式碼

為簡單起見,Fraud Detection 服務的埠設定為 8080,應用執行在 8090 埠。

如果您此時執行測試,它會失敗,因為當前沒有服務執行在 8080 埠上。

在本地克隆 Fraud Detection 服務倉庫

您可以從試用服務端合同開始。為此,您必須首先透過執行以下命令克隆它

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

在本地的 Fraud Detection 服務倉庫中定義合同

作為消費方,您需要定義您究竟想要實現什麼。您需要明確您的期望。為此,編寫以下合同

將合同放在 src/test/resources/contracts/fraud 資料夾中。fraud 資料夾很重要,因為生產方的測試基類名稱會引用該資料夾。

以下示例顯示了我們的合同,包括 Groovy 和 YAML 格式

YML 合同非常直觀。然而,當您檢視使用靜態型別 Groovy DSL 編寫的合同時,您可能會想知道 value(client(…​), server(…​)) 部分是什麼。透過使用這種表示法,Spring Cloud Contract 允許您定義 JSON 塊、URL 或其他結構的動態部分。在識別符號或時間戳的情況下,您無需硬編碼一個值。您希望允許不同的值範圍。要啟用值範圍,您可以為消費方設定匹配這些值的正則表示式。您可以透過 map 表示法或帶插值的 String 來提供 body。我們強烈建議使用 map 表示法。

要設定合同,您必須理解 map 表示法。請參閱 Groovy 關於 JSON 的文件

之前顯示的合同是雙方之間的協議,內容如下

  • 如果傳送一個 HTTP 請求,包含以下所有內容

    • /fraudcheck 端點上使用 PUT 方法

    • 一個 JSON body,其中 client.id 匹配正則表示式 [0-9]{10},且 loanAmount 等於 99999

    • 一個 Content-Type 頭部,其值為 application/vnd.fraud.v1+json

  • 則會向消費方傳送一個 HTTP 響應,該響應

    • 狀態為 200

    • 包含一個 JSON body,其中 fraudCheckStatus 欄位的值為 FRAUDrejectionReason 欄位的值為 Amount too high

    • 包含一個 Content-Type 頭部,其值為 application/vnd.fraud.v1+json

一旦您準備好在整合測試中實際檢查 API,您需要在本地安裝 stubs。

新增 Spring Cloud Contract Verifier 外掛

我們可以新增 Maven 或 Gradle 外掛。在本例中,我們展示如何新增 Maven。首先,我們新增 Spring Cloud Contract BOM,如下例所示

接下來,新增 Spring Cloud Contract Verifier Maven 外掛,如下例所示

由於添加了外掛,您將獲得 Spring Cloud Contract Verifier 功能,這些功能會根據提供的合同

  • 生成並執行測試

  • 生成並安裝 stubs

您不需要生成測試,因為作為消費方,您只需要使用 stubs。您需要跳過測試的生成和呼叫。為此,請執行以下命令

$ 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

以下這行非常重要

[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 的 stubs 已經安裝到本地倉庫。

執行整合測試

為了利用 Spring Cloud Contract Stub Runner 的自動下載 stub 功能,您必須在您的消費方專案(Loan Application service)中執行以下操作

  1. 新增 Spring Cloud Contract BOM,如下所示

  2. 新增對 Spring Cloud Contract Stub Runner 的依賴,如下所示

  3. 使用 @AutoConfigureStubRunner 註解您的測試類。在註解中,提供 group-idartifact-id,以便 Stub Runner 下載您的協作服務的 stubs。

  4. (可選)由於您是在離線狀態下使用協作服務,您也可以提供離線工作開關(StubRunnerProperties.StubsMode.LOCAL)。

現在,當您執行測試時,您會在日誌中看到類似以下輸出

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 找到了您的 stubs,併為您的應用啟動了一個伺服器,其 group ID 為 com.example,artifact ID 為 http-server,使用了 0.0.1-SNAPSHOT 版本的 stubs,classifier 為 stubs,埠為 8080

提交 Pull Request

您到目前為止所做的是一個迭代過程。您可以試用合同,將其安裝在本地,並在消費方繼續工作,直到合同按您的期望工作。

一旦您對結果感到滿意並且測試透過,您可以向服務端釋出 Pull Request。目前,消費方的工作已完成。

生產方 (Fraud Detection 伺服器)

作為 Fraud Detection 伺服器(Loan Issuance 服務的伺服器)的開發者,您可能需要

  • 接管 Pull Request

  • 編寫缺失的實現

  • 部署應用

以下 UML 圖顯示了欺詐檢測流程

getting-started-cdc-server

接管 Pull Request

提醒一下,以下清單顯示了初始實現

然後您可以執行以下命令

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

您必須新增自動生成測試所需的依賴項,如下所示

在 Maven 外掛的配置中,您必須傳遞 packageWithBaseClasses 屬性,如下所示

本例透過設定 packageWithBaseClasses 屬性使用“基於約定”的命名。這樣做意味著最後兩個包組合起來構成測試基類的名稱。在本例中,合同位於 src/test/resources/contracts/fraud 下。由於從 contracts 資料夾開始沒有兩個包,因此只選取一個,即 fraud。新增 Base 字尾並將 fraud 首字母大寫。這樣就得到了 FraudBase 測試類名稱。

所有生成的測試都擴充套件該類。在那裡,您可以設定您的 Spring Context 或任何必要的內容。在本例中,您應該使用 Rest Assured MVC 來啟動服務端 FraudDetectionController。以下清單顯示了 FraudBase

現在,如果您執行 ./mvnw clean install,您將看到類似以下輸出

Results :

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

此錯誤發生是因為您有一個新的合同,並從中生成了測試,由於您尚未實現該功能,測試失敗了。自動生成的測試將看起來像以下測試方法

@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(…​)) 塊中的 producer() 部分都被注入到了測試中。如果您使用 YAML,對於 responsematchers 部分也同樣適用。

請注意,在生產方,您也在進行 TDD。期望以測試的形式表達出來。該測試會向我們自己的應用傳送合同中定義的 URL、頭部和 body 的請求。它還期望響應中包含精確定義的值。換句話說,您處於紅、綠、重構(red, green, refactor)中的 red 階段。是時候將 red 轉換為 green 了。

編寫缺失的實現

因為您知道預期的輸入和輸出,您可以按照以下方式編寫缺失的實現

當您再次執行 ./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 的命令,這將釋出應用和 stub artifacts。

消費方 (Loan Issuance),最後一步

作為 loan issuance 服務(Fraud Detection 伺服器的消費方)的開發者,您需要

  • 將我們的功能分支合併到 master

  • 切換到線上工作模式

以下 UML 圖顯示了流程的最終狀態

getting-started-cdc-client-final

合併分支到 Master

以下命令展示了使用 Git 將分支合併到 master 的一種方法

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

線上工作

現在您可以停用 Spring Cloud Contract Stub Runner 的離線工作模式,並指定您的 stubs 倉庫的位置。此時,服務端的 stubs 會自動從 Nexus/Artifactory 下載。您可以將 stubsMode 的值設定為 REMOTE。以下程式碼展示了透過更改屬性實現同樣效果的示例

就是這樣。您已完成本教程。