什么是契約測試
測試是軟件流程中非常重要,不可或缺的一個環(huán)節(jié)。一般的測試分為單元測試召耘,集成測試,端到端的手工測試褐隆,這也是構(gòu)成測試金字塔的三個層級污它。我們今天將要討論的話題是契約測試,它是處于單元測試和集成測試中間的一個環(huán)節(jié)妓灌。這三個層級分別測試的場景如下:
- 單元測試:測試單個service
- 集成測試:測試由多個services組成的系統(tǒng)
- 端到端測試:測試從用戶到各個外部系統(tǒng)的整個場景
契約測試的作用:
- 測試接口和接口之間的正確性
- 驗(yàn)證服務(wù)層提供的數(shù)據(jù)是否是消費(fèi)端所需要的
- 將本來需要在集成測試中體現(xiàn)的問題前移轨蛤,更早的發(fā)現(xiàn)問題
- 更快速的驗(yàn)證消費(fèi)端和提供端之間交互的基本正確性
為什么要存在契約測試
首先我們將使用以下示例模型來描述微服務(wù)測試背后的概念:
在上面的圖中,我們可以看到有兩個微服務(wù)虫埂,通過REST彼此進(jìn)行通信。第一項(xiàng)服務(wù)扮演消費(fèi)者的角色圃验,第二項(xiàng)扮演提供者的角色掉伏。
當(dāng)需要進(jìn)行集成測試時,可以通過服務(wù)虛擬化來模擬正在與之通信的微服務(wù)。這里服務(wù)提供者被模擬斧散,在部署消費(fèi)者服務(wù)之前供常,您希望證明其能正常工作。當(dāng)運(yùn)行所有測試均為綠色您認(rèn)為可以部署您的服務(wù)了鸡捐。
但是栈暇,如果您針對生產(chǎn)提供商運(yùn)行服務(wù),而不是模擬版本箍镜,則有可能會失敗源祈。在這個例子中,提供者已經(jīng)改變了數(shù)據(jù)格式色迂。集成測試無法解決這個問題香缺,因?yàn)樗鼈冋卺槍rovider的過時版本運(yùn)行。
如何填補(bǔ)測試過程中的這個空白歇僧?將引入消費(fèi)者驅(qū)動契約測試的概念图张。消費(fèi)者驅(qū)動契約測試方法是在消費(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是一個開源的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)建的契約文檔提供對應(yīng)格式的數(shù)據(jù)并返回給消費(fèi)者饶辙,通過契約檢查判斷如果服務(wù)端提供的數(shù)據(jù)和消費(fèi)者生成的契約不匹配蹲诀,將拋出異常并提示給服務(wù)提供端。
Spring Cloud Contract
Spring Cloud Contract是一個基于消費(fèi)者驅(qū)動契約的測試框架弃揽。它會基于契約來生成存根服務(wù)脯爪,消費(fèi)方不需要等待接口開發(fā)完成则北,就可以通過存根服務(wù)完成集成測試。Spring Could Contract中痕慢,契約是用一種基于 Groovy 的 DSL 定義的尚揣。
談到契約測試時,我們首先需要定義一個包含期望使用接口的第一個文件掖举。作為標(biāo)準(zhǔn)PACT法則快骗,契約必須由消費(fèi)者服務(wù)來定義,但是在Spring Cloud Contract中塔次,它實(shí)際上位于提供者服務(wù)代碼中方篮。在指南手冊中包含了兩個大步驟:
服務(wù)提供者
- 編寫合同規(guī)范(Groovy DSL)
- 在Provider端生成自動驗(yàn)收測試
- 生成WireMock JSON存根&將存根發(fā)布到Maven(本地)存儲庫
服務(wù)消費(fèi)者
- 在消費(fèi)者端配置Stub Runner
- 執(zhí)行消費(fèi)者測試 - Stub Runner嵌入了WireMock
- 檢查驗(yàn)證結(jié)果
服務(wù)提供者
我們在服務(wù)端編寫一個簡單服務(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 依賴
對于我們的提供者俺叭,我們需要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上下文的測試包中添加一個基類:
@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)建時熄守,運(yùn)行 mvn clean install 插件會自動生成一個名為ContractVerifierTest的測試類蜈垮,它擴(kuò)展我們的BaseTestClass并將其放在/target/generated-test-sources/contracts/中。
測試方法的名稱派生自前綴“ validate_”與我們的Groovy測試存根的名稱連接裕照。對于上面的Groovy文件攒发,生成的方法名稱將為“validate_shouldReturnEvenWhenRequestParamIsEven”。
我們來看看這個自動生成的測試類:
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存儲庫中添加存根jar晋南,以便我們的消費(fèi)者可以使用它惠猿。
服務(wù)消費(fèi)者
我們的CDC消費(fèi)者將通過HTTP交互生成的存根來維護(hù)契約,因此提供者方面的任何更改都將破壞契約负间。
新建BasicMathController偶妖,它將發(fā)出HTTP請求以從生成的存根中獲取響應(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 依賴
對于我們的消費(fèi)者,我們需要添加spring-cloud-contract-wiremock和spring-cloud-contract-stub-runner依賴項(xiàng)政溃。還有本地Maven存儲庫中的可用存根:
<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)在是時候配置我們的存根運(yùn)行器趾访,它將通知我們的消費(fèi)者如何調(diào)用我們本地Maven存儲庫中的可用存根:
@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自動注入StubRunner,模擬服務(wù)方。
參數(shù)ids定位到maven中的stub.jar董虱。
Ids = groupId : artifactId : version(’+’表示最新版本): 存根 : StubRunner端口
如果你將stub.jar發(fā)布到Maven私服中扼鞋,可以通過repositoryRoot參數(shù)指定私服地址來遠(yuǎn)程調(diào)用。在測試通過后會根據(jù)契約返回響應(yīng)內(nèi)容愤诱。
總結(jié)
文中首先介紹了契約測試的背景以及基于CDC開發(fā)服務(wù)的大致過程云头。然后編寫契約文件通過Spring Cloud Contract的contract verifier插件生成存根和服務(wù)提供方的測試用例,消費(fèi)方編寫測試用例淫半,通過StrubRunner模擬服務(wù)方來完成一次消費(fèi)方調(diào)用服務(wù)方的測試溃槐。
本文包含的代碼地址