取得領先
VMware 提供培訓和認證,以加速您的進度。
了解更多本文簡要回顧了什麼是 Contract Testing (契約測試)、Spring Cloud Contract 如何實作它,以及 Spring Cloud Contract 如何在多語言環境中使用。
為了增加我們系統行為正確的確定性,我們會編寫不同類型的測試。根據 測試金字塔,主要的測試類型是單元測試、整合測試和 UI 測試。測試越複雜,所需的時間和精力就越多,而且它們變得越脆弱。
在分散式系統中,最常見的問題之一是測試應用程式之間的整合。假設您的服務向另一個應用程式發送 REST 請求。使用 Spring Boot 時,您可以編寫一個 @SpringBootTest
來測試該行為。您設置一個 Spring 上下文,準備一個要發送的請求……並將其發送到哪裡?您尚未啟動另一個應用程式,因此您會收到 Connection Refused
異常。您可以嘗試模擬真實的 HTTP 呼叫並返回虛假的響應。但是,如果您這樣做,您不會測試任何實際的 HTTP 整合、序列化和反序列化機制等等。您也可以啟動一個虛假的 HTTP 伺服器(例如,WireMock)並模擬它的行為方式。這裡的問題是,作為 API 的客戶端,您定義了伺服器的行為方式。換句話說,如果您告訴虛假伺服器在向端點 /myEndpoint
發送請求時返回文字 testText
,它就會這樣做,即使真實伺服器沒有這樣的端點。簡而言之,問題在於存根 (stubs) 可能不可靠。
另一個問題是與第三方系統的整合。可能存在一個共享實例,由於高負載,每 5 分鐘就會崩潰一次。在這種情況下,我們希望將該系統存根化,以便它不會影響我們的整合測試,但我們需要這些存根是可靠的。
建立一個用於端對端測試的環境、產生所有應用程式並通過運行整個系統來執行測試始終很誘人。通常,這是一個很好的解決方案,可以提高您對業務功能仍然正常運作的信心。但是,端對端測試的問題是它們經常在沒有明顯原因的情況下失敗,並且速度非常慢。沒有什麼比看到在運行了 10 個小時後,由於 API 呼叫中的一個錯字而導致端對端測試失敗更令人沮喪的了。
這個問題的一個潛在解決方案是契約測試 (Contract Tests)。在我們深入了解這些測試的細節之前,讓我們先定義一些術語
生產者 (producer): 伺服器端擁有者(例如,HTTP API 的擁有者)或通過佇列(例如 RabbitMQ)發送訊息的生產者。
消費者 (consumer): 消耗 HTTP API 或監聽通過(例如)RabbitMQ 接收的訊息的應用程式。
契約 (contract): 生產者和消費者之間關於通訊方式的協議。它不是一個架構 (schema)。它更像是一種使用情境。例如,對於這個特定的情境,我期望一個指定的輸入,然後我回覆一個指定的輸出。
契約測試 (contract test): 一種驗證生產者和消費者可以彼此整合的測試。這並不意味著該功能正常運作。這種區別很重要,因為您不希望通過為每個功能編寫契約來重複您的工作。契約測試斷言生產者和消費者之間的整合滿足契約中定義的需求。它們的主要優點是它們快速且可靠。
以下範例顯示了用 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)。它們之間的主要區別在於生產者和消費者的合作方式。
在生產者契約 (Producer Contract) 測試方法中,生產者定義契約並編寫契約測試,描述 API,並發布存根,而無需與其客戶端進行任何合作。通常,當 API 是公開的,並且 API 的所有者甚至不知道誰在使用它時,就會發生這種情況。一個例子是 Spring Initializr,它使用 Spring Rest Docs 測試發布其存根。版本 0.5.0.BUILD-SNAPSHOT
的存根可在此處找到,帶有 stubs
分類器。
在消費者驅動契約 (Consumer-Driven Contract) 測試方法中,契約由消費者建議,並與生產者密切合作。生產者確切地知道哪個消費者定義了哪個契約,以及當契約相容性被破壞時,哪個契約會被破壞。當使用內部 API 時,這種方法更常見。
在這兩種情況下,契約都可以定義在生產者的儲存庫中(使用 DSL 定義或通過編寫契約測試),也可以定義在存儲所有契約的外部儲存庫中。
由於現在使用 Spring Cloud Contract 進行非 JVM 專案要容易得多,因此最好解釋封裝預設背後的基本術語並介紹 Maven 命名法。
提示
Apache Maven 是一個軟體專案管理和理解工具。基於專案物件模型 (POM) 的概念,Maven 可以從一條中心資訊管理專案的建置、報告和文件。(請參閱 https://maven.apache.org/)
(以下部分定義取自 Maven 詞彙表。)
專案 (Project)
:Maven 以專案的角度思考。您建置的所有內容都是專案。這些專案遵循定義明確的「專案物件模型」。專案可以依賴於其他專案,在這種情況下,後者稱為「依賴項」。一個專案可能包含多個子專案。但是,這些子專案仍然被視為專案。
Artifact (構件)
:構件是專案產生或使用的東西。Maven 為專案產生的構件範例包括 JAR 和原始碼和二進位發行版。每個構件都由組 ID 和構件 ID 唯一標識,構件 ID 在組內是唯一的。
JAR
: JAR 代表 Java ARchive (Java 歸檔)。它是一種基於 ZIP 檔案格式的格式。Spring Cloud Contract 將契約和產生的存根封裝在 JAR 檔案中。
GroupId (群組ID)
: 群組 ID 是專案的通用唯一標識符。雖然這通常是專案名稱(例如,commons-collections
),但使用完全限定的套件名稱將其與名稱相似的其他專案區分開來(例如,org.apache.maven
)很有幫助。通常,當發布到構件管理器時,GroupId
會以斜線分隔並構成 URL 的一部分。例如,對於 com.example
的群組 ID,application
的構件 ID 將是 /com/example/application/
。
Classifier (分類器)
: Maven 依賴項表示法如下所示:groupId:artifactId:version:classifier
。分類器是傳遞給依賴項的額外後綴(例如 stubs
或 sources
)。相同的依賴項(例如,com.example:application
)可以產生多個構件,這些構件的分類器彼此不同。
Artifact manager (構件管理器)
: 當您產生二進位檔案、原始碼或套件時,您希望它們可供其他人下載、引用或重複使用。在 JVM 世界中,這些構件將是 JAR。對於 Ruby,這些將是 gem。對於 Docker,這些將是 Docker 映像。您可以將這些構件存儲在管理器中。此類管理器的範例包括 Artifactory 和 Nexus。
Spring Cloud Contract 是一個總括專案,包含協助使用者實作各種契約測試的解決方案。它主要有兩個模組:Spring Cloud Contract Verifier
,主要由生產者端 (producer side) 使用;以及 Spring Cloud Contract Stub Runner
,由消費者端 (consumer side) 使用。
此專案讓您可以使用以下方式定義契約:
假設我們決定使用 YAML 來撰寫契約。在生產者端,從契約中:
使用 Maven 或 Gradle 外掛程式產生測試,以確認符合契約。
產生 Stub 供其他專案重複使用。
以下為生產者契約方法 (producer contract approach) 的簡化流程,適用於使用 Spring Cloud Contract 且以 YAML 撰寫契約的 JVM 應用程式。
生產者:
套用 Maven 或 Gradle Spring Cloud Contract 外掛程式。
在 src/test/resources/contracts/
下定義 YAML 契約。
從契約產生測試和 Stub。
建立一個繼承產生的測試並設定測試環境 (test context) 的基礎類別。
一旦測試通過,就建立一個具有 stubs
分類器的 JAR,其中儲存契約和 Stub。
將具有 stubs
分類器的 JAR 上傳到二進位儲存空間。
消費者:
使用 Stub Runner 取得生產者的 Stub。Stub Runner 會啟動記憶體中的 HTTP 伺服器 (預設情況下,這些是 WireMock 伺服器),並以 Stub 餵入。
針對 Stub 執行測試。
因此,使用 Spring Cloud Contract 和契約測試可提供您:
Stub 的可靠性:它們僅在測試通過後才會產生。
Stub 的可重複使用性:它們可以由多個消費者下載和重複使用。
分散式系統是由以不同語言和架構編寫的應用程式所建立的。「問題」之一是 Spring Cloud Contract 的 DSL 必須以 Groovy 撰寫。即使契約不需要任何關於該語言的特殊知識,對於非 JVM 使用者來說,這仍然成為一個問題。
在生產者端,Spring Cloud Contract 會以 Java 或 Groovy 產生測試。當然,在非 JVM 環境中使用這些測試會成為一個問題。您不僅需要安裝 Java,而且測試是使用 Maven 或 Gradle 外掛程式產生的,這需要使用這些建置工具。
從 Edgware.SR2
發布列車版本和 1.2.3.RELEASE
版本的 Spring Cloud Contract 開始,我們決定新增功能,以便在非 JVM 世界中更廣泛地採用 Spring Cloud Contract。
我們新增了使用 YAML 撰寫契約的支援。YAML 是一種 (又是另一種) 標記語言,不綁定任何特定語言,並且已被廣泛使用。這應該可以解決使用與任何特定語言相關的 DSL 定義契約的「問題」。
為了隱藏實作細節 (例如 Java 測試的產生、外掛程式設定或 Java 安裝),我們需要引入一個抽象層。我們決定使用 Docker 映像來隱藏這些細節。我們將所有專案設定、所需套件和資料夾結構封裝在 Docker 映像中,如此一來,使用者除了所需的環境變數之外,不需要任何其他知識。
我們為 生產者 和 消費者 引入了 Docker 映像。所有與 JVM 相關的邏輯都被封裝在 Docker 容器中,這表示您不必安裝 Java 即可產生測試並使用 Stub Runner 執行 Stub。
以下章節將介紹一個使用 Spring Cloud Contract 測試的 NodeJS 應用程式的範例。該程式碼從 https://github.com/bradtraversy/bookstore 分支而來,並可在 https://github.com/spring-cloud-samples/spring-cloud-contract-nodejs 下取得。我們的目標是以最少的努力,盡可能快速地開始為現有應用程式產生測試和 Stub。
讓我們複製簡單的 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,從這些契約產生的測試需要依序執行。Stub 是有狀態的,這表示只有在執行與 1_shouldAddABook
相符的要求後,2_shouldReturnListOfBooks.yml
才能從 Stub HTTP 伺服器取得。
重要事項
在真實的範例中,我們會在契約測試模式下執行 NodeJS 應用程式,其中對資料庫的呼叫將被 Stub 化,並且不需要有狀態的 Stub。在此範例中,我們想展示如何在短時間內從 Spring Cloud Contract 中受益。
讓我們看看其中一個 Stub:
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
該契約指出,如果將 POST
要求傳送到 /api/books
,標頭為 Content-Type: application/json
且主體如上所述,則回應應為 200
。現在,在執行契約測試之前,讓我們分析 Spring Cloud Contract Docker 映像的需求。
該映像可在 DockerHub 的 SpringCloud 組織下取得。
一旦您掛載您的契約並傳遞環境變數,該映像將:
產生契約測試。
針對提供的 URL 執行測試。
產生 WireMock Stub。
將 Stub 發布到 Artifact Manager。(此步驟是可選的,但預設情況下會開啟。)
重要事項
產生的測試假設您的應用程式正在執行並且已準備好在指定的連接埠上監聽要求。這表示您必須在執行契約測試之前執行它。
Docker 映像會在 /contracts
資料夾下搜尋契約。執行測試的輸出可在 /spring-cloud-contract/build
資料夾下取得 (它對於除錯很有用)。執行建置時,您需要掛載這些磁碟區。
Docker 映像也需要一些環境變數,這些變數指向您正在執行的應用程式、Artifact Manager 實例和其他變數,如下列清單所述:
PROJECT_GROUP
:您專案的群組 ID。預設為 com.example
。
PROJECT_VERSION
:您專案的版本。預設為 0.0.1-SNAPSHOT
。
PROJECT_NAME
。Artifact ID。預設為 example
。
REPO_WITH_BINARIES_URL
- 您的 Artifact Manager 的 URL。預設為 [https://127.0.0.1:8081/artifactory/libs-release-local](https://127.0.0.1:8081/artifactory/libs-release-local)
,這是本機執行時 Artifactory 的預設 URL。
REPO_WITH_BINARIES_USERNAME
:(可選) Artifact Manager 受到保護時的使用者名稱。
REPO_WITH_BINARIES_PASSWORD
:(可選) Artifact Manager 受到保護時的密碼。
PUBLISH_ARTIFACTS
:如果設定為 true
,則將 Artifact 發布到二進位儲存空間。預設為 true
。
以下環境變數在執行測試時使用:
APPLICATION_BASE_URL
:測試執行的 URL。請記得,它必須可以從 Docker 容器存取 (localhost
無效)。
APPLICATION_USERNAME
:(選用)用於應用程式基本驗證的使用者名稱。
APPLICATION_PASSWORD
:(選用)用於應用程式基本驗證的密碼。
重要事項
要執行此範例,您需要安裝 Docker、Docker Compose 和 npm。
由於我們想要執行測試,所以可以使用
$ 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 腳本會發生什麼事
基礎建設 (MongoDb, Artifactory) 被設定。
由於我們沒有在 NodeJS 應用程式中模擬資料庫的限制,因此契約也代表了有狀態的情況
第一個請求是 POST
,它會導致資料插入到資料庫中。
第二個請求是 GET
,它會傳回一個資料清單,其中包含先前插入的一個元素。
NodeJS 應用程式已啟動 (在埠 3000
上),並且可以在 192.168.0.100
取得。
契約測試由 Docker 產生,並且針對正在執行的應用程式執行測試。
契約取自 /contracts
資料夾。
測試執行的輸出可在 node_modules/spring-cloud-contract/output
取得。
存根已上傳到 Artifactory。您可以在 https://127.0.0.1:8081/artifactory/libs-release-local/com/example/bookstore/0.0.1.RELEASE/ 檢視它們。 存根位於 https://127.0.0.1:8081/artifactory/libs-release-local/com/example/bookstore/0.0.1.RELEASE/bookstore-0.0.1.RELEASE-stubs.jar。
總之,我們定義了 YAML 契約,執行了 NodeJS 應用程式,並執行了 Docker 映像檔以產生契約測試和存根,並將它們上傳到 Artifactory。
在此範例中,我們發布了一個 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
上執行 bookstore 應用程式的存根。 為此,讓我們使用以下存根執行 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
。
從位於 [http://192.168.0.100:8081/artifactory/libs-release-local](http://192.168.0.100:8081/artifactory/libs-release-local)
的 Artifactory 下載存根。
在埠 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://127.0.0.1:9876/api/books
# Now it's time for the second request
$ curl -X GET https://127.0.0.1:9876/api/books
# You should receive the contents of the JSON
總之,一旦存根被上傳,您就可以使用一些環境變數執行 Docker 映像檔,並在您的整合測試中重複使用它們,而無需考慮所使用的程式語言。
在這篇部落格文章中,我們解釋了什麼是契約測試以及它們為何重要。 我們介紹了如何使用 Spring Cloud Contract 來產生和執行契約測試。 最後,我們介紹了一個關於如何將 Spring Cloud Contract Docker 映像檔用於 producer 和 consumer 的非 JVM 應用程式的範例。
閱讀 Spring Cloud Contract 的文件。
查看 Bookstore 範例。