Spring Cloud Contract 在多語言世界中

工程 | Marcin Grzejszczak | 2018年2月13日 | ...

本文簡要回顧了契約測試是什麼、Spring Cloud Contract 如何實現契約測試以及 Spring Cloud Contract 如何在多語言世界中使用。

什麼是契約測試

為了提高系統正常執行的確定性,我們編寫了不同型別的測試。根據測試金字塔,主要的測試型別包括單元測試、整合測試和 UI 測試。測試越複雜,所需的度和精力就越多,並且會變得越脆弱。

在分散式系統中,最常見的問題之一是測試應用程式之間的整合。假設您的服務向另一個應用程式傳送 REST 請求。使用 Spring Boot 時,您可以編寫 @SpringBootTest 來測試該行為。您設定了 Spring 上下文,準備了要傳送的請求……​然後您將其傳送到哪裡?您還沒有啟動另一個應用程式,因此會收到 Connection Refused 異常。您可以嘗試模擬真實的 HTTP 呼叫並返回偽造的響應。但是,如果您這樣做,您就沒有測試任何真實的 HTTP 整合、序列化和反序列化機制等等。您也可以啟動一個偽造的 HTTP 伺服器(例如,WireMock)並模擬其應有的行為。這裡的問題在於,作為 API 的客戶端,您定義了伺服器的行為方式。換句話說,如果您告訴偽造的伺服器在請求傳送到端點 /myEndpoint 時返回文字 testText,它就會照做,即使真實的伺服器沒有這樣的端點也是如此。簡而言之,問題在於存根可能不可靠。

另一個問題是與第三方系統的整合。可能存在由於高負載而每 5 分鐘就崩潰一次的共享例項。在這種情況下,我們希望將該系統存根化,使其不影響我們的整合測試,但我們需要這些存根是可靠的。

設定端到端測試環境、啟動所有應用程式並透過執行整個系統來執行測試總是很有吸引力。通常,這是一個很好的解決方案,可以增加您對業務功能仍在正常工作的信心。然而,端到端測試的問題在於它們經常無故失敗並且非常慢。沒有比看到運行了十個小時後,端到端測試由於 API 呼叫中的拼寫錯誤而失敗更令人沮喪的事情了。

解決此問題的一種潛在方法是契約測試。在我們詳細介紹這些測試之前,讓我們先定義一些術語

  • 生產者:伺服器端所有者(例如,HTTP API 的所有者)或透過佇列(例如 RabbitMQ)傳送訊息的生產者。

  • 消費者:使用 HTTP API 或監聽透過(例如)RabbitMQ 接收的訊息的應用程式。

  • 契約:生產者和消費者之間關於通訊應如何進行的約定。它不是一個模式(schema)。它更像是一個使用場景。例如,對於這個特定的場景,我期望一個指定的輸入,然後我回復一個指定的輸出。

  • 契約測試:一種驗證生產者和消費者之間可以相互整合的測試。它不代表功能正常工作。這種區別很重要,因為您不會想透過為每個功能編寫契約來重複工作。契約測試斷言生產者和消費者之間的整合滿足契約中定義的要求。它們的主要優點是快速且可靠。

以下示例顯示了一個用 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 running a request in the integration test, we can interpret that test as follows:
#
#(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 `client.id` field
# * has a `loanAmount` field that is equal to `99999`
#(5) - with `Content-Type` header equal to `application/json`
#(6) - and a `client.id` json entry matches a regular expression of `[0-9]{10}`
#(7) - then the response is sent with
#(8) - status equal to `200`
#(9) - and JSON body equal to
# { "fraudCheckStatus": "FRAUD", "rejection.reason": "Amount too high" }
#(10) - with header `Content-Type` equal to `application/json`
#
#From the Producer perspective, in the autogenerated producer-side test, we can interpret that test as follows:
#
#(1) - A request is sent to the producer
#(2) - With the "PUT" method
#(3) - to the URL "/fraudcheck"
#(4) - with the JSON body that
# * has a `client.id` field with a value of `1234567890`
# * has a `loanAmount` field with a value of `99999`
#(5) - with a `Content-Type` header equal to `application/json`
#(7) - then the test asserts if the response has been sent with
#(8) - status equal `200`
#(9) - and a JSON body equal to
# { "fraudCheckStatus": "FRAUD", "rejection.reason": "Amount too high" }
#(10) - with a `Content-Type` header equal to `application/json;charset=UTF-8`

本文重點介紹兩種主要的契約測試型別:生產者契約測試(Producer Contract testing)和消費者驅動契約測試(Consumer-Driven Contract testing)。它們之間的主要區別在於生產者和消費者的協作方式。

  • 生產者契約測試方法中,生產者定義契約並編寫契約測試,描述 API,並在不與其客戶端進行任何協作的情況下發布存根。通常,當 API 是公開的且 API 所有者甚至不知道誰正在使用它時,就會發生這種情況。例如,Spring Initializr 透過 Spring Rest Docs 測試釋出其存根。0.5.0.BUILD-SNAPSHOT 版本的存根可透過此處帶有 stubs 分類器的檔案獲取。

  • 消費者驅動契約測試方法中,契約由消費者與生產者密切合作提出。生產者清楚地知道哪個消費者定義了哪個契約,以及當契約相容性被破壞時哪個契約會受到影響。當使用內部 API 時,這種方法更常見。

在這兩種情況下,契約既可以在生產者的倉庫中定義(透過 DSL 定義或編寫契約測試),也可以在一個儲存所有契約的外部倉庫中定義。

Maven 術語介紹

由於現在使用 Spring Cloud Contract 處理非 JVM 專案變得容易得多,因此最好解釋一下打包預設值背後的一些基本術語,並介紹一下 Maven 術語。

提示

Apache Maven 是一個軟體專案管理和理解工具。Maven 基於專案物件模型 (POM) 的概念,可以從中心資訊塊管理專案的構建、報告和文件。(請參閱 https://maven.apache.org/

(以下部分定義摘自Maven 術語表。)

  • Project(專案):Maven 以專案的形式進行思考。您構建的所有東西都是專案。這些專案遵循一個明確定義的“專案物件模型”(Project Object Model)。專案可以依賴於其他專案,在這種情況下,後者稱為“依賴項”(dependencies)。一個專案可能由幾個子專案組成。但是,這些子專案仍然被視為專案。

  • Artifact(構件):構件是專案產生或使用的東西。Maven 為專案產生的構件示例包括 JAR 檔案、原始碼分發包和二進位制分發包。每個構件都由 group ID 和 artifact ID 唯一標識,其中 artifact ID 在一個 group 中是唯一的。

  • JAR:JAR 代表 Java ARchive(Java 歸檔)。它是一種基於 ZIP 檔案格式的格式。Spring Cloud Contract 將契約和生成的存根打包在一個 JAR 檔案中。

  • GroupId(組 ID):組 ID 是專案的全域性唯一識別符號。雖然這通常是專案名稱(例如,commons-collections),但使用完全限定的包名稱有助於將其與具有類似名稱的其他專案區分開來(例如,org.apache.maven)。通常,當釋出到 Artifact Manager 時,GroupId 會用斜槓分隔並構成 URL 的一部分。例如,對於 group ID com.exampleapplication 的 artifact ID 將是 /com/example/application/

  • Classifier(分類器):Maven 依賴項的表示法如下所示:groupId:artifactId:version:classifier。分類器是傳遞給依賴項的附加字尾(例如 stubssources)。同一個依賴項(例如 com.example:application)可以產生多個因分類器而彼此不同的構件。

  • Artifact manager(構件管理器):生成二進位制檔案、原始碼或軟體包後,您會希望其他人可以下載、引用或重用它們。在 JVM 世界中,這些構件是 JAR。對於 Ruby,它們是 gems。對於 Docker,它們是 Docker 映象。您可以將這些構件儲存在管理器中。此類管理器的示例包括 ArtifactoryNexus

什麼是 Spring Cloud Contract

Spring Cloud Contract 是一個傘形專案,包含幫助使用者實現各種契約測試的解決方案。它有兩個主要模組:主要由生產者方使用的 Spring Cloud Contract Verifier,以及由消費者方使用的 Spring Cloud Contract Stub Runner

該專案允許您使用以下方式定義契約:

假設我們決定使用 YAML 編寫契約。在生產者端,根據契約,

  • 透過 Maven 或 Gradle 外掛生成測試,以斷言契約得到滿足。

  • 生成存根供其他專案重用。

對於使用 Spring Cloud Contract 和 YAML 契約的 JVM 應用程式,生產者契約方法的簡化流程如下。

生產者:

  • 應用 Maven 或 Gradle Spring Cloud Contract 外掛。

  • src/test/resources/contracts/ 下定義 YAML 契約。

  • 從契約生成測試和存根。

  • 建立擴充套件生成測試並設定測試上下文的基類。

  • 測試通過後,建立一個帶有 stubs 分類器的 JAR 檔案,其中儲存了契約和存根。

  • 將帶有 stubs 分類器的 JAR 檔案上傳到二進位制儲存庫。

消費者:

  • 使用 Stub Runner 獲取生產者的存根。Stub Runner 會啟動記憶體中的 HTTP 伺服器(預設情況下是 WireMock 伺服器),並載入存根。

  • 針對存根執行測試。

因此,使用 Spring Cloud Contract 和契約測試為您提供了

  • 存根可靠性:它們只有在測試通過後才生成。

  • 存根可重用性:它們可以被多個消費者下載和重用。

Spring Cloud Contract 當前存在的問題

分散式系統由用不同語言和框架編寫的應用程式構建。Spring Cloud Contract 的一個“問題”是 DSL 必須用 Groovy 編寫。儘管契約不需要任何特定的語言知識,但這對於非 JVM 使用者來說成為了一個問題。

在生產者端,Spring Cloud Contract 會生成 Java 或 Groovy 測試。當然,在非 JVM 環境中使用這些測試就成了一個問題。您不僅需要安裝 Java,而且測試是透過 Maven 或 Gradle 外掛生成的,這需要使用這些構建工具。

Spring Cloud Contract 和多語言支援

Edgware.SR2 釋出列和 Spring Cloud Contract 的 1.2.3.RELEASE 版本開始,我們決定新增一些功能,以便 Spring Cloud Contract 在非 JVM 世界中得到更廣泛的應用。

我們添加了對使用 YAML 編寫契約的支援。YAML 是另一種標記語言,它不限於任何特定語言,並且已被廣泛使用。這應該能解決使用與特定語言相關的 DSL 定義契約的“問題”。

為了隱藏實現細節(例如生成 Java 測試、外掛設定或 Java 安裝),我們需要引入一個抽象層。我們決定透過使用 Docker 映象來隱藏這些細節。我們將所有專案設定、所需的軟體包和資料夾結構封裝在一個 Docker 映象中,這樣除了必需的環境變數之外,使用者無需瞭解其他任何知識。

我們為生產者消費者引入了 Docker 映象。所有與 JVM 相關的邏輯都被封裝在一個 Docker 容器中,這意味著您無需安裝 Java 即可生成測試並使用 Stub Runner 執行存根。

以下部分將透過一個使用 Spring Cloud Contract 進行測試的 NodeJS 應用程式示例進行介紹。程式碼從 https://github.com/bradtraversy/bookstore 分叉而來,並在 https://github.com/spring-cloud-samples/spring-cloud-contract-nodejs 下可用。我們的目標是以最少的精力盡快為現有應用程式開始生成測試和存根。

Spring Cloud Contract 在生產者端的使用

讓我們克隆一個簡單的 NodeJS MVC 應用程式,如下所示:

$ git clone https://github.com/spring-cloud-samples/spring-cloud-contract-nodejs
$ cd spring-cloud-contract-nodejs

它連線到 Mongo DB 資料庫來儲存圖書資料。

YAML 契約可在 /contracts 資料夾下找到,如下所示:

$ ls contracts
1_shouldAddABook.yml          2_shouldReturnListOfBooks.yml

數字字尾告訴 Spring Cloud Contract,從這些契約生成的測試需要按順序執行。存根是狀態化的,這意味著只有在執行了與 1_shouldAddABook 匹配的請求後,才能從存根化的 HTTP 伺服器獲取 2_shouldReturnListOfBooks.yml

重要提示

在實際示例中,我們會以契約測試模式執行 NodeJS 應用程式,其中對資料庫的呼叫將被存根化,並且無需狀態化的存根。在本示例中,我們希望展示如何快速利用 Spring Cloud Contract。

讓我們看一下其中一個存根:

description: |
  Should add a book
request:
  method: POST
  url: /api/books
  headers:
    Content-Type: application/json
  body: '{
    "title" : "Title",
    "genre" : "Genre",
    "description" : "Description",
    "author" : "Author",
    "publisher" : "Publisher",
    "pages" : 100,
    "image_url" : "https://d213dhlpdb53mu.cloudfront.net/assets/pivotal-square-logo-41418bd391196c3022f3cd9f3959b3f6d7764c47873d858583384e759c7db435.svg",
    "buy_url" : "https://pivotal.io"
  }'
response:
  status: 200

契約規定,如果向 /api/books 傳送一個帶有 Content-Type: application/json 頭部和上述請求體的 POST 請求,則響應應該是 200。現在,在執行契約測試之前,讓我們分析一下 Spring Cloud Contract docker 映象的要求。

Spring Cloud Contract Docker 映象

該映象可在 DockerHub 的 SpringCloud 組織下獲取。

掛載契約並傳遞環境變數後,該映象會:

  • 生成契約測試。

  • 針對提供的 URL 執行測試。

  • 生成 WireMock 存根。

  • 將存根釋出到構件管理器。(此步驟是可選的,但預設啟用。)

重要提示

生成的測試假設您的應用程式正在執行並準備好在指定的埠上監聽請求。這意味著您必須在執行契約測試之前執行它。

Spring Cloud Contract Docker 映象設定

Docker 映象會在 /contracts 資料夾下搜尋契約。執行測試的輸出可在 /spring-cloud-contract/build 資料夾下獲取(這對於除錯很有用)。執行構建時需要掛載這些卷。

Docker 映象還需要一些環境變數,指向您正在執行的應用程式、構件管理器例項以及其他一些變數,如下所述:

  • PROJECT_GROUP:您的專案的 group ID。預設為 com.example

  • PROJECT_VERSION:您的專案的版本。預設為 0.0.1-SNAPSHOT

  • PROJECT_NAME:構件 ID。預設為 example

  • REPO_WITH_BINARIES_URL - 您的構件管理器的 URL。預設為 [https://:8081/artifactory/libs-release-local](https://:8081/artifactory/libs-release-local),這是 Artifactory 在本地執行時預設的 URL。

  • REPO_WITH_BINARIES_USERNAME:(可選)構件管理器需要認證時的使用者名稱。

  • REPO_WITH_BINARIES_PASSWORD:(可選)構件管理器需要認證時的密碼。

  • PUBLISH_ARTIFACTS:如果設定為 true,則將構件釋出到二進位制儲存庫。預設為 true

執行測試時會使用以下環境變數:

  • APPLICATION_BASE_URL:應針對其執行測試的 URL。請記住,它必須可以從 Docker 容器訪問(localhost 不起作用)。

  • APPLICATION_USERNAME:(可選)應用程式基本認證的使用者名稱。

  • APPLICATION_PASSWORD:(可選)應用程式基本認證的密碼。

在生產者端執行 Spring Cloud Contract 測試

重要提示

要執行此示例,您需要安裝 DockerDocker Composenpm

既然我們要執行測試,可以使用

$ npm install
$ npm test

然而,為了學習目的,我們將其分解為以下幾個部分(我們將分析每個 bash 指令碼行):

# Install the required npm packages
$ npm install

# Stop docker infra (mongodb, artifactory)
$ ./stop_infra.sh
# Start docker infra (mongodb, artifactory)
$ ./setup_infra.sh

# Kill & Run app
$ pkill -f "node app"
$ nohup node app &

# Prepare environment variables
$ export SC_CONTRACT_DOCKER_VERSION="1.2.3.RELEASE"
$ export APP_IP="192.168.0.100" # This has to be the IP that is available outside of Docker container
$ export APP_PORT="3000"
$ export ARTIFACTORY_PORT="8081"
$ export APPLICATION_BASE_URL="http://${APP_IP}:${APP_PORT}"
$ export ARTIFACTORY_URL="http://${APP_IP}:${ARTIFACTORY_PORT}/artifactory/libs-release-local"
$ export CURRENT_DIR="$( pwd )"
$ export PROJECT_NAME="bookstore"
$ export PROJECT_GROUP="com.example"
$ export PROJECT_VERSION="0.0.1.RELEASE"

# Execute contract tests
$ docker run  --rm -e "APPLICATION_BASE_URL=${APPLICATION_BASE_URL}" \
-e "PUBLISH_ARTIFACTS=true" -e "PROJECT_NAME=${PROJECT_NAME}" \
-e "PROJECT_GROUP=${PROJECT_GROUP}" -e "REPO_WITH_BINARIES_URL=${ARTIFACTORY_URL}" \
-e "PROJECT_VERSION=${PROJECT_VERSION}" -v "${CURRENT_DIR}/contracts/:/contracts:ro" \
-v "${CURRENT_DIR}/node_modules/spring-cloud-contract/output:/spring-cloud-contract-output/" \
springcloud/spring-cloud-contract:"${SC_CONTRACT_DOCKER_VERSION}"

# Kill app
$ pkill -f "node app"

透過 bash 指令碼,將發生以下情況:

總而言之,我們定義了 YAML 契約,運行了 NodeJS 應用程式,並運行了 Docker 映象來生成契約測試和存根,然後將它們上傳到 Artifactory。

在消費者端使用 Spring Cloud Contract 存根

在此示例中,我們釋出了一個 spring-cloud/spring-cloud-contract-stub-runner Docker 映象,該映象會啟動 Stub Runner 的獨立版本。

提示

如果您習慣於執行 java -jar 命令而不是執行 Docker,可以從 Maven 下載獨立的 JAR(例如,版本 1.2.3.RELEASE),如下所示:wget -O stub-runner.jar 'https://search.maven.org/remote_content?g=org.springframework.cloud&a=spring-cloud-contract-stub-runner-boot&v=1.2.3.RELEASE'

您可以將任何 屬性 作為環境變數傳遞。慣例是將所有字母大寫,並將單詞分隔符和點號 (.) 替換為下劃線 (_)。例如,stubrunner.repositoryRoot 屬性應表示為 STUBRUNNER_REPOSITORY_ROOT 環境變數。

假設我們想在埠 9876 上執行圖書應用程式的存根。為此,我們按如下方式使用存根執行 Stub Runner Boot 應用程式:

# Provide the Spring Cloud Contract Docker version
$ export SC_CONTRACT_DOCKER_VERSION="1.2.3.RELEASE"
# The IP at which the app is running and the Docker container can reach it
$ export APP_IP="192.168.0.100"
# Spring Cloud Contract Stub Runner properties
$ export STUBRUNNER_PORT="8083"
# Stub coordinates 'groupId:artifactId:version:classifier:port'
$ export STUBRUNNER_IDS="com.example:bookstore:0.0.1.RELEASE:stubs:9876"
$ export STUBRUNNER_REPOSITORY_ROOT="http://${APP_IP}:8081/artifactory/libs-release-local"
# Run the docker with Stub Runner Boot
$ docker run  --rm -e "STUBRUNNER_IDS=${STUBRUNNER_IDS}" \
-e "STUBRUNNER_REPOSITORY_ROOT=${STUBRUNNER_REPOSITORY_ROOT}" \
-p "${STUBRUNNER_PORT}:${STUBRUNNER_PORT}" -p "9876:9876" \
springcloud/spring-cloud-contract-stub-runner:"${SC_CONTRACT_DOCKER_VERSION}"

該指令碼會:

  • 啟動一個獨立的 Spring Cloud Contract Stub Runner 應用程式。

  • 使 Stub Runner 下載具有以下座標的存根:com.example:bookstore:0.0.1.RELEASE:stubs

  • 從 Artifactory 下載存根,地址為 [http://192.168.0.100:8081/artifactory/libs-release-local](http://192.168.0.100:8081/artifactory/libs-release-local)

  • (延遲後)在埠 8083 啟動 Stub Runner。

  • 在埠 9876 執行存根。

在伺服器端,我們構建了一個有狀態的存根。讓我們使用 curl 來斷言存根設定正確,如下所示:

# let's execute the first request (no response is returned)
$ curl -H "Content-Type:application/json" -X POST \
--data '{ "title" : "Title", "genre" : "Genre", "description" : "Description", "author" : "Author", "publisher" : "Publisher", "pages" : 100, "image_url" : "https://d213dhlpdb53mu.cloudfront.net/assets/pivotal-square-logo-41418bd391196c3022f3cd9f3959b3f6d7764c47873d858583384e759c7db435.svg", "buy_url" : "https://pivotal.io" }' https://:9876/api/books
# Now it's time for the second request
$ curl -X GET https://:9876/api/books
# You should receive the contents of the JSON

總而言之,一旦存根上傳完畢,您就可以執行帶有幾個環境變數的 Docker 映象,並在您的整合測試中重複使用它們,無論使用何種程式語言。

總結

在這篇博文中,我們解釋了契約測試是什麼以及它們的重要性。我們展示瞭如何使用 Spring Cloud Contract 生成和執行契約測試。最後,我們透過一個示例介紹瞭如何為非 JVM 應用程式在生產者和消費者端使用 Spring Cloud Contract Docker 映象。

附加資源

獲取 Spring 新聞通訊

訂閱 Spring 新聞通訊保持聯絡

訂閱

領先一步

VMware 提供培訓和認證,助您加速發展。

瞭解更多

獲取支援

Tanzu Spring 透過一個簡單的訂閱提供對 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位制檔案。

瞭解更多

即將舉行的活動

檢視 Spring 社群所有即將舉行的活動。

檢視全部