什么是契約測試
測試是軟件流程中非常重要,不可或缺的一個(gè)環(huán)節(jié)。一般的測試分為單元測試,集成測試,端到端的手工測試,這也是構(gòu)成測試金字塔的三個(gè)層級(jí)。我們今天將要討論的話題是契約測試,它是處于單元測試和集成測試中間的一個(gè)環(huán)節(jié)。這三個(gè)層級(jí)分別測試的場景如下:
- 單元測試:測試單個(gè)service
- 集成測試:測試由多個(gè)services組成的系統(tǒng)
- 端到端測試:測試從用戶到各個(gè)外部系統(tǒng)的整個(gè)場景
契約測試的作用:
- 測試接口和接口之間的正確性
- 驗(yàn)證服務(wù)層提供的數(shù)據(jù)是否是消費(fèi)端所需要的
- 將本來需要在集成測試中體現(xiàn)的問題前移,更早的發(fā)現(xiàn)問題
- 更快速的驗(yàn)證消費(fèi)端和提供端之間交互的基本正確性
為什么要存在契約測試
首先我們將使用以下示例模型來描述微服務(wù)測試背后的概念:

在上面的圖中,我們可以看到有兩個(gè)微服務(wù),通過REST彼此進(jìn)行通信。第一項(xiàng)服務(wù)扮演消費(fèi)者的角色,第二項(xiàng)扮演提供者的角色。
當(dāng)需要進(jìn)行集成測試時(shí),可以通過服務(wù)虛擬化來模擬正在與之通信的微服務(wù)。這里服務(wù)提供者被模擬,在部署消費(fèi)者服務(wù)之前,您希望證明其能正常工作。當(dāng)運(yùn)行所有測試均為綠色您認(rèn)為可以部署您的服務(wù)了。

但是,如果您針對(duì)生產(chǎn)提供商運(yùn)行服務(wù),而不是模擬版本,則有可能會(huì)失敗。在這個(gè)例子中,提供者已經(jīng)改變了數(shù)據(jù)格式。集成測試無法解決這個(gè)問題,因?yàn)樗鼈冋卺槍?duì)Provider的過時(shí)版本運(yùn)行。

如何填補(bǔ)測試過程中的這個(gè)空白?將引入消費(fèi)者驅(qū)動(dòng)契約測試的概念。消費(fèi)者驅(qū)動(dòng)契約測試方法是在消費(fèi)者和提供者之間定義在它們彼此之間轉(zhuǎn)移的數(shù)據(jù)格式。通常,合同的格式由消費(fèi)者定義并與相應(yīng)的提供商共享。之后,執(zhí)行測試以驗(yàn)證契約是否相符。CDC測試的先決條件之一是可以與提供商服務(wù)團(tuán)隊(duì)保持良好的最佳密切溝通,分享這些契約和交流測試結(jié)果是實(shí)施適當(dāng)?shù)腃DC測試的重要部分。
PACT測試框架
PACT是一個(gè)開源的CDC測試框架。它提供了廣泛的語言支持,如Ruby,Java,Scala,.NET,Javascript,Swift/Objective-C。
PACT的工作原理
消費(fèi)者作為數(shù)據(jù)的最終使用者非常清楚、明確的知道需要的什么樣格式,什么類型的數(shù)據(jù),它將負(fù)責(zé)創(chuàng)建契約文檔(包含結(jié)構(gòu)和格式的json文件),服務(wù)提供端將根據(jù)消費(fèi)者端創(chuàng)建的契約文檔提供對(duì)應(yīng)格式的數(shù)據(jù)并返回給消費(fèi)者,通過契約檢查判斷如果服務(wù)端提供的數(shù)據(jù)和消費(fèi)者生成的契約不匹配,將拋出異常并提示給服務(wù)提供端。
Spring Cloud Contract
Spring Cloud Contract是一個(gè)基于消費(fèi)者驅(qū)動(dòng)契約的測試框架。它會(huì)基于契約來生成存根服務(wù),消費(fèi)方不需要等待接口開發(fā)完成,就可以通過存根服務(wù)完成集成測試。Spring Could Contract中,契約是用一種基于 Groovy 的 DSL 定義的。
談到契約測試時(shí),我們首先需要定義一個(gè)包含期望使用接口的第一個(gè)文件。作為標(biāo)準(zhǔn)PACT法則,契約必須由消費(fèi)者服務(wù)來定義,但是在Spring Cloud Contract中,它實(shí)際上位于提供者服務(wù)代碼中。在指南手冊(cè)中包含了兩個(gè)大步驟:
服務(wù)提供者
- 編寫合同規(guī)范(Groovy DSL)
- 在Provider端生成自動(dòng)驗(yàn)收測試
- 生成WireMock JSON存根&將存根發(fā)布到Maven(本地)存儲(chǔ)庫
服務(wù)消費(fèi)者
- 在消費(fèi)者端配置Stub Runner
- 執(zhí)行消費(fèi)者測試 - Stub Runner嵌入了WireMock
- 檢查驗(yàn)證結(jié)果
服務(wù)提供者
我們?cè)诜?wù)端編寫一個(gè)簡單服務(wù)接口,判斷數(shù)字是奇數(shù)還是偶數(shù)
@RestController
public class EvenOddController {
@GetMapping("/validate/prime-number")
public String isNumberPrime(@RequestParam("number") Integer number) {
return number % 2 == 0 ? "Even" : "Odd";
}
}
MAVEN 依賴
對(duì)于我們的提供者,我們需要spring-cloud-starter-contract-verifier依賴:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-verifier</artifactId>
<scope>test</scope>
</dependency>
需要將我們的基礎(chǔ)測試類的名稱配置到spring-cloud-contract-maven-plugin:
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>1.2.2.RELEASE</version>
<extensions>true</extensions>
<configuration>
<baseClassForTests>com.peterwanghao.spring.cloud.contract.producer.BaseTestClass
</baseClassForTests>
</configuration>
</plugin>
基礎(chǔ)測試類
需要在加載Spring上下文的測試包中添加一個(gè)基類:
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@DirtiesContext
@AutoConfigureMessageVerifier
public class BaseTestClass {
@Autowired
private EvenOddController evenOddController;
@Before
public void setup() {
StandaloneMockMvcBuilder standaloneMockMvcBuilder = MockMvcBuilders.standaloneSetup(evenOddController);
RestAssuredMockMvc.standaloneSetup(standaloneMockMvcBuilder);
}
}
測試存根
在/src/test/ resources/contracts/目錄中,我們將在groovy文件中添加測試存根。例如
import org.springframework.cloud.contract.spec.Contract
Contract.make {
description "should return even when number input is even"
request {
method GET()
url("/validate/prime-number") {
queryParameters {
parameter("number", "2")
}
}
}
response {
body("Even")
status 200
}
}
當(dāng)我們運(yùn)行構(gòu)建時(shí),運(yùn)行 mvn clean install 插件會(huì)自動(dòng)生成一個(gè)名為ContractVerifierTest的測試類,它擴(kuò)展我們的BaseTestClass并將其放在/target/generated-test-sources/contracts/中。
測試方法的名稱派生自前綴“ validate_”與我們的Groovy測試存根的名稱連接。對(duì)于上面的Groovy文件,生成的方法名稱將為“validate_shouldReturnEvenWhenRequestParamIsEven”。
我們來看看這個(gè)自動(dòng)生成的測試類:
public class ContractVerifierTest extends BaseTestClass {
@Test
public void validate_shouldReturnEvenWhenRequestParamIsEven() throws Exception {
// given:
MockMvcRequestSpecification request = given();
// when:
ResponseOptions response = given().spec(request)
.queryParam("number","2")
.get("/validate/prime-number");
// then:
assertThat(response.statusCode()).isEqualTo(200);
// and:
String responseBody = response.getBody().asString();
assertThat(responseBody).isEqualTo("Even");
}
}
構(gòu)建還將在我們的本地Maven存儲(chǔ)庫中添加存根jar,以便我們的消費(fèi)者可以使用它。
服務(wù)消費(fèi)者
我們的CDC消費(fèi)者將通過HTTP交互生成的存根來維護(hù)契約,因此提供者方面的任何更改都將破壞契約。
新建BasicMathController,它將發(fā)出HTTP請(qǐng)求以從生成的存根中獲取響應(yīng):
@RestController
public class BasicMathController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("/calculate")
public String checkOddAndEven(@RequestParam("number") Integer number) {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add("Content-Type", "application/json");
ResponseEntity<String> responseEntity = restTemplate.exchange(
"http://localhost:8090/validate/prime-number?number=" + number, HttpMethod.GET,
new HttpEntity<>(httpHeaders), String.class);
return responseEntity.getBody();
}
}
MAVEN 依賴
對(duì)于我們的消費(fèi)者,我們需要添加spring-cloud-contract-wiremock和spring-cloud-contract-stub-runner依賴項(xiàng)。還有本地Maven存儲(chǔ)庫中的可用存根:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-wiremock</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-stub-runner</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.peterwanghao.spring.cloud</groupId>
<artifactId>spring-cloud-contract-producer</artifactId>
<version>0.0.1-SNAPSHOT</version>
<scope>test</scope>
</dependency>
存根運(yùn)行器
現(xiàn)在是時(shí)候配置我們的存根運(yùn)行器,它將通知我們的消費(fèi)者如何調(diào)用我們本地Maven存儲(chǔ)庫中的可用存根:
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureMockMvc
@AutoConfigureJsonTesters
@AutoConfigureStubRunner(workOffline = true, ids = "com.peterwanghao.spring.cloud:spring-cloud-contract-producer:+:stubs:8090")
public class BasicMathControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Test
public void given_WhenPassEvenNumberInQueryParam_ThenReturnEven() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/calculate?number=2").contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk()).andExpect(content().string("Even"));
}
}
通過@AutoConfigureStubRunner自動(dòng)注入StubRunner,模擬服務(wù)方。
參數(shù)ids定位到maven中的stub.jar。
Ids = groupId : artifactId : version(’+’表示最新版本): 存根 : StubRunner端口
如果你將stub.jar發(fā)布到Maven私服中,可以通過repositoryRoot參數(shù)指定私服地址來遠(yuǎn)程調(diào)用。在測試通過后會(huì)根據(jù)契約返回響應(yīng)內(nèi)容。
總結(jié)
文中首先介紹了契約測試的背景以及基于CDC開發(fā)服務(wù)的大致過程。然后編寫契約文件通過Spring Cloud Contract的contract verifier插件生成存根和服務(wù)提供方的測試用例,消費(fèi)方編寫測試用例,通過StrubRunner模擬服務(wù)方來完成一次消費(fèi)方調(diào)用服務(wù)方的測試。
本文包含的代碼地址