調用方和服務方約定好接口,生成映射文件,這個文件即可以用於客戶端模擬服務,也可以用於服務方集成測試,這樣雙方開發也好、集成也好都會方便很多。下面我們來研究一下 Spring Cloud Contract,它就是基於 WireMock 實現了契約式的測試,上文中雙方約定好的接口,其實就是雙方的契約。
微服務的集成
前面已經提到,傳統方式下,微服務的集成以及測試都是一件很頭痛的事情。其實在微服務概念還沒有出現之前,在 SOA 流行的時候,就有人提出了消費者驅動契約(Consumer Driven Contract,CDC)的概念。微服務流行后,服務的集成和集成測試成了不得不解決問題,於是出現了基於消費者驅動契約的測試工具,最流行的應該就是 Pact,還有就是今天我們要說的 Spring Cloud Contract。
消費者驅動契約
熟悉敏捷開發的同學應該知道,敏捷開發提倡測試先行,相應的提出了不少方法和流程,例如測試驅動開發(Test Driven Design,TDD)、驗收測試驅動開發(Acceptance Test Driven Development,ATDD)、行為驅動設計(Behavior Driven Design,BDD )、實例化需求(Specification By Example)等等。它們的共同特點在開發前就約定好了各種形式的契約。如果是單元測試作為契約,就是 TDD;如果是驗收測試作為契約,就是 ATDD;如果是形式化語言甚至圖表定義的業務規則,那就是 BDD 或者實例化需求。
對於基於 HTTP 的微服務來說,它的契約就是指 API 的請求和響應的規則。對於請求,包括請求 URL 及參數,請求頭,請求內容等;對於響應,包括狀態碼,響應頭,響應內容等。
在 Spring Cloud Contract 里,契約是用一種基於 Groovy 的 DSL 定義的。例如下面是一個短信接口的契約(省略了部分內容,例如 Content-Type 頭等)。
org.springframework.cloud.contract.spec.Contract.make { // 如果消費方發送了一個請求 request { // 請求方法是 POST method 'POST' // 請求 URL 是 `/sendsms` url '/sendsms' // 請求內容是 Json 文本,包括電話號碼和要發送的文本 body([ // 電話號碼必須是13個數字組成 phone: $(regex('[0-9]{13}')), // 發送文本必須為"您好" content: "您好" ]) } response { // 那么服務方應該返回狀態碼 200 status 200 // 響應內容是 Json 文本,內容為 { "success": true } body([ success: true ]) }}
使用 CDC 開發服務的大致過程是這樣的。
-
編寫契約(Groovy 的 DSL 定義)(提供方)---業務方和服務方相關人員一起討論。業務方告知服務方接口使用的場景、期望的返回是什么,服務方考慮接口方案和實現,雙方一起定下一個或多個契約。
-
契約提供者自驗證(提供方)---確定了契約之后,Spring Cloud Contract 會給服務方自動生成驗收測試,用於驗證接口是否符合契約。服務方要確保開發完成后,這些驗收測試都能夠通過。
-
消費方通過stub進行集成測試(消費方)---服務消費方也可以基於這個契約開始開發功能。Spring Cloud Contract 會基於契約生成 Stub 服務,這樣業務方就不必等接口開發完成,可以通過 Stub 服務進行集成測試。
所以 CDC 和行為驅動設計(BDD)很類似,都是從使用者的需求出發,雙方訂立契約,測試先行的開發方法。不過一個是針對系統的驗收,一個是針對服務的集成。CDC 的好處有以下幾點:
-
讓服務方和調用方有充分的溝通,確保服務方提供接口都是以調用方的需求出發,並且服務方的開發者也可以充分理解調用方的使用場景。
-
解耦和服務方和調用方的開發過程,一旦契約訂立,雙方都可以並行開發,通過 Mock 和自動化集成測試確保雙方都遵守契約,最終集成也會更簡單。
-
通過 Mock 和自動化測試,可以確保雙方在演進過程中,也不會破壞已有的契約。
但是要注意一點是,契約不包括業務邏輯,業務邏輯還是需要服務方和調用方通過單元測試、其他集成測試來確保。例如上面的短信服務,可能服務方會有一個邏輯是每天一個號碼最多發送一條短信,但這個邏輯並不會包含在契約里,可能契約只有包含成功和錯誤兩種情況。
Spring Cloud Contract 使用方法
我使用的
springboot是1.5.10.RELEASE
springcloud是Edgware.SR3
服務提供方:
第一步:環境准備:JarSpring Cloud Contract 支持 Gradle 和 Maven,下面是gradle
1、buildscript的dependencies中增加:
testCompile 'org.springframework.cloud:spring-cloud-contract-gradle-plugin:1.2.7.RELEASE'
testCompile 'org.springframework.cloud:spring-cloud-starter-contract-verifier'
testCompile 'org.springframework.cloud:spring-cloud-starter-contract-stub-runner'
2、增加plugin:
apply plugin: 'application'
apply plugin: 'groovy'
apply plugin: 'spring-cloud-contract'
apply plugin: 'maven'
apply plugin: 'maven-publish'
3、publishing:
publishing { repositories { repositories { maven { url 'https://nexus.xxx.net/repository/contract-test/' credentials { username 'user' password 'xxxx' } } } } publications { maven(MavenPublication) { //指定group/artifact/version信息,可以不填。默認使用項目group/name/version作為groupId/artifactId/version groupId "$project.group" artifactId "$project.name" version "1.0.0-SNAPSHOT" artifact verifierStubsJar } } }
第二步:編寫契約,下面2個示例
groovy定義的示例:在/src/test/resources/contracts目錄下,創建test.groovy文件:
import org.springframework.cloud.contract.spec.Contract Contract.make { request { method GET() urlPath('/api/v1/getUserInfo') { queryParameters { parameter('memberId', 'abc123') } } headers { contentType('application/json') header('myHeader', 'duan') } } response { status 200 body([ "respCode": "000000", "respMsg": "success" ]) headers { //contentType('application/json') } } }
示例sms:
org.springframework.cloud.contract.spec.Contract.make { // 如果消費方發送了一個請求 request { // 請求方法是 POST method 'POST' // 請求 URL 是 `/sendsms` url '/sendsms' // 請求內容是 Json 文本,包括電話號碼和要發送的文本 body([ // 電話號碼必須是13個數字組成 phone: $(regex('[0-9]{13}')), // 發送文本必須為"您好" content: "您好" ]) } response { // 那么服務方應該返回狀態碼 200 status 200 // 響應內容是 Json 文本,內容為 { "success": true } body([ success: true ]) }}
第三步:服務提供者自驗證
對於服務提供方,Spring Cloud Contract 提供了一個叫 Contarct Verifier 的東西,用於解析契約文件生成測試代碼,可以通過運行該測試代碼進行契約的自驗證。
testCompile 'org.springframework.cloud:spring-cloud-starter-contract-verifier'
如果使用 Gradle 的話,通過以下命令生成測試。
./gradlew generateContractTests
或者gradle test
在build下生產junit的測試類:

上面發送短信的契約,生成的測試代碼是這樣的。如果有多個groovy文件,在ContractVerifierTest就會有多個方法
public class SmsTest extends ConstractTestBase { @Test public void validate_sendsms() throws Exception { // given: MockMvcRequestSpecification request = given() .body("{\"phone\":\"2066260255168\",\"content\":\"\u60A8\u597D\"}"); // when: ResponseOptions response = given().spec(request) .post("/sendsms"); // then: assertThat(response.statusCode()).isEqualTo(200); // and: DocumentContext parsedJson = JsonPath.parse(response.getBody().asString()); assertThatJson(parsedJson).field("['success']").isEqualTo(true); } }
可以看到是一個很標准的 JUnit 測試,使用了 RestAssured 來測試 API 接口。其中的 ConstractTestBase是設置的測試基類,里面可以做一些配置以及 Setup 和 Teardown 操作。例如這里,我們需要用 RestAssured 來啟動 Spring 的 webApplicationContext,當然我也可以用 standaloneSetup 設置啟動單個 Controller。
------------ConstractTestBase說明--------------------------------------
1、基類 上面自動生成的類的基類是需要自己編寫
package com.xxx.xxx.contract;
import org.junit.Before; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.web.context.WebApplicationContext; import com.xxx.xxx.Application; import io.restassured.module.mockmvc.RestAssuredMockMvc; @RunWith(SpringRunner.class) @SpringBootTest(classes = Application.class) //@ActiveProfiles("unit-test") //當契約測試時想使用不同的配置文件時,取消注釋時會使用loan-unit-test.yml配置文件 public abstract class ConstractTestBase { static{ System.setProperty("aes.xxx", "abc"); } @Autowired private WebApplicationContext context; @Before public void setUp() throws Exception { RestAssuredMockMvc.webAppContextSetup(context); } }
2、該類需要配置在gradle中:
contracts { baseClassForTests = 'com.xxx.xxx.contract.ConstractTestBase' }
如果沒有基類及baseClassForTests 的配置,在執行契約自驗證時,會出現錯誤:
java.lang.IllegalStateException: You haven't configured a MockMVC instance. You can do this statically
大概就是無法初始化MockMVC了。
------------ConstractTestBase說明--------------------------------------
執行契約的自驗證測試的方法是:gradle test
查看報告方法:上面gradle test運行結束后,有個結果報告位置,打開這個html就可以看到了
首先我們需要在服務方通過以下命令生成 Stub 服務的 Jar 包。
d:\gitspace\loan (master -> origin) λ gradle verifierStubsJar Starting a Gradle Daemon (subsequent builds will be faster) :copyContracts :generateClientStubs :verifierStubsJar BUILD SUCCESSFUL Total time: 18.885 secs d:\gitspace\loan (master -> origin)

現在開始上傳契約的stub到maven私服,提供給消費方使用。在項目根目錄下執行gradle publish即可。
服務調用方
在上面生產的stubs的 Jar 包里面包含了契約文件以及生成的 WireMock 映射文件。我們可以把它發布到 Maven 私庫里去,這樣調用方可以直接從私庫下載 Stub 的 Jar 包。
對於調用方,Spring Cloud Contract 提供了 Stub Runner 來簡化 Stub 的使用。
@RunWith(SpringRunner.class) @SpringBootTest(classes = Application.class, webEnvironment = SpringBootTest.WebEnvironment.MOCK) @ActiveProfiles("unit-test") @AutoConfigureStubRunner( repositoryRoot="https://xxx.xxx.net/repository/contract-test", ids = "com.xxx:loan:1.0.0-SNAPSHOT:stubs:8090") public class ContractTest { @Test public void testSendSms() { ResponseEntity<SmsServiceResponse> response = restTemplate.exchange("http://localhost:6565/sendsms", HttpMethod.POST, new HttpEntity<>(request), SmsServiceResponse.class); // do some verification } }
注意注解 AutoConfigureStubRunner,里面設置了下載 Stub Jar 包的私庫地址以及包的完整 ID,注意最后的 6565 就是指定 Stub 運行的本地端口。測試的時候訪問 Stub 端口,就會根據契約返回內容。
還有一種方式:在/src/test/resources目錄下增加一個配置文件application-stub.yml文件:
stubrunner: stubs-mode: REMOTE repositoryRoot: https://xxx.xxx.net/repository/contract-test username: user password: xxx ids: - com.xxx.xxx:1.0.0-SNAPSHOT:stubs:8090
在該配置下的junit寫法如下:
import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.json.AutoConfigureJsonTesters; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit4.SpringRunner; import com.xxx.xxx.xxx.web.test.TestController; @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK) @AutoConfigureMockMvc @AutoConfigureJsonTesters @AutoConfigureStubRunner @ActiveProfiles("stub") //讀取/src/test/resources/application-stub.yml的配置,遠程使用默認的default //@Ignore public class ConsumerLoanContractTest { @Autowired private TestController testController; @Test public void pay() { String result = testController.testLoan("abc123"); System.out.println("result=" + result); assert "0".equals(result); } }
前端開發
另外一個使用 Mock 的場景就是對於前端開發。以前,前端工程師一般需要自己創建 Mock 數據進行開發,但 Mock 數據很容易和后台最終提供的數據有不一致的地方。CDC 和 Spring Cloud Contract 也可以幫上忙。
Spring Cloud Contract 生成的 Stub 其實是 WireMock 的映射文件,因此直接使用 WireMock 也是可以的。不過,它還提供了使用 Spring Cloud Cli 運行 Stub 的方式。
首先需要安裝 SpringBoot Cli 和 Spring Cloud Cli,Mac 下可以使用 Homebrew。
$ brew tap pivotal/tap
$ brew install springboot
$ spring install org.springframework.cloud:spring-cloud-cli:1.4.0.RELEASE
然后在當前目錄創建一個 stubrunner.yml 配置文件,里面的配置參數和前面的 AutoConfigureStubRunner 的配置其實是一樣的:
stubrunner:
workOffline: false
repositoryRoot: http://<nexus_root>
ids:
- com.xingren.service:sms-client-stubs:1.5.0-SNAPSHOT:stubs:6565
最后運行 spring cloud stubrunner,即可啟動 Stub 服務。前端同學就可以愉快的使用 Stub 來進行前端開發了。
DSL
Spring Cloud Contract 的契約 DSL,既可以用於生成服務方的測試,也可以用於生成供調用方使用的 Stub,但是這兩種方式對數據的驗證方法有一些不同。對於服務方測試,DSL 需要提供請求內容,驗證響應;而對於 Stub,DSL 需要匹配請求,提供響應內容。Spring Cloud Contract 提供了幾種方式來處理。
一種方式是通過 $(consumer(...), producer(...)) 的語法(或者$(stub(...), test(...))、$(client(...), server(...))、$(c(...), p(...)),都是一樣的)。例如:
org.springframework.cloud.contract.spec.Contract.make {
request {
method('GET')
url $(consumer(~/\/[0-9]{2}/), producer('/12'))
}
response {
status 200
body(
name: $(consumer('Kowalsky'), producer(regex('[a-zA-Z]+')))
)
}
}
上面就是指對於調用方,url 需要匹配 ~/\/[0-9]{2}/ 這個正則表達式,Stub 就會返回響應,其中 name 則為 Kowalsky。而對於服務方,生產的測試用例的請求 url 為 /12,它會驗證響應中的 name 符合正則 '[a-zA-Z]+'。另外,Spring Cloud Contract 還提供了 stubMatchers 和 testMatchers 來支持更復雜的請求匹配和測試驗證。
Spring Cloud Contract 現在還在快速發展中,目前對於生成測試用例的規則,還是有不夠靈活的地方。例如,對於某些 Stub 應該返回,但生成的測試里不需要驗證的字段,支持不太完善。還有對於 form-urlencoded 的請求,處理起來不如 Json 的請求那么方便。相信后繼版本會改善。
總結
通過上面簡單介紹,我們可以看到基於 Spring Cloud Contract 以及契約測試的方法,可以讓微服務之間以及前后端之間的集成更順暢。
另外前面還提到 Pact,它的優勢是支持多種語言,但我們的環境都是基於 JVM 的,而 Spring Cloud Contract 和 SpringBoot 以及 Junit 的集成更簡單方便。而且 Spring Cloud Contract 的另一個優勢是它可以自動生成服務方的自動化測試。
參考:https://blog.csdn.net/M2l0ZgSsVc7r69eFdTj/article/details/79068939