什麼是契約測試
測試是軟件流程中非常重要,不可或缺的一個環節。一般的測試分爲單元測試,集成測試,端到端的手工測試,這也是構成測試金字塔的三個層級。我們今天將要討論的話題是契約測試,它是處於單元測試和集成測試中間的一個環節。這三個層級分別測試的場景如下:
- 單元測試:測試單個service
- 集成測試:測試由多個services組成的系統
- 端到端測試:測試從用戶到各個外部系統的整個場景
契約測試的作用:
- 測試接口和接口之間的正確性
- 驗證服務層提供的數據是否是消費端所需要的
- 將本來需要在集成測試中體現的問題前移,更早的發現問題
- 更快速的驗證消費端和提供端之間交互的基本正確性
爲什麼要存在契約測試
首先我們將使用以下示例模型來描述微服務測試背後的概念:
在上面的圖中,我們可以看到有兩個微服務,通過REST彼此進行通信。第一項服務扮演消費者的角色,第二項扮演提供者的角色。
當需要進行集成測試時,可以通過服務虛擬化來模擬正在與之通信的微服務。這裏服務提供者被模擬,在部署消費者服務之前,您希望證明其能正常工作。當運行所有測試均爲綠色您認爲可以部署您的服務了。
但是,如果您針對生產提供商運行服務,而不是模擬版本,則有可能會失敗。在這個例子中,提供者已經改變了數據格式。集成測試無法解決這個問題,因爲它們正在針對Provider的過時版本運行。
如何填補測試過程中的這個空白?將引入消費者驅動契約測試的概念。消費者驅動契約測試方法是在消費者和提供者之間定義在它們彼此之間轉移的數據格式。通常,合同的格式由消費者定義並與相應的提供商共享。之後,執行測試以驗證契約是否相符。CDC測試的先決條件之一是可以與提供商服務團隊保持良好的最佳密切溝通,分享這些契約和交流測試結果是實施適當的CDC測試的重要部分。
PACT測試框架
PACT是一個開源的CDC測試框架。它提供了廣泛的語言支持,如Ruby,Java,Scala,.NET,Javascript,Swift/Objective-C。
PACT的工作原理
消費者作爲數據的最終使用者非常清楚、明確的知道需要的什麼樣格式,什麼類型的數據,它將負責創建契約文檔(包含結構和格式的json文件),服務提供端將根據消費者端創建的契約文檔提供對應格式的數據並返回給消費者,通過契約檢查判斷如果服務端提供的數據和消費者生成的契約不匹配,將拋出異常並提示給服務提供端。
Spring Cloud Contract
Spring Cloud Contract是一個基於消費者驅動契約的測試框架。它會基於契約來生成存根服務,消費方不需要等待接口開發完成,就可以通過存根服務完成集成測試。Spring Could Contract中,契約是用一種基於 Groovy 的 DSL 定義的。
談到契約測試時,我們首先需要定義一個包含期望使用接口的第一個文件。作爲標準PACT法則,契約必須由消費者服務來定義,但是在Spring Cloud Contract中,它實際上位於提供者服務代碼中。在指南手冊中包含了兩個大步驟:
服務提供者
- 編寫合同規範(Groovy DSL)
- 在Provider端生成自動驗收測試
- 生成WireMock JSON存根&將存根發佈到Maven(本地)存儲庫
服務消費者
- 在消費者端配置Stub Runner
- 執行消費者測試 - Stub Runner嵌入了WireMock
- 檢查驗證結果
服務提供者
我們在服務端編寫一個簡單服務接口,判斷數字是奇數還是偶數
@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>
需要將我們的基礎測試類的名稱配置到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>
基礎測試類
需要在加載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
}
}
當我們運行構建時,運行 mvn clean install 插件會自動生成一個名爲ContractVerifierTest的測試類,它擴展我們的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");
}
}
構建還將在我們的本地Maven存儲庫中添加存根jar,以便我們的消費者可以使用它。
服務消費者
我們的CDC消費者將通過HTTP交互生成的存根來維護契約,因此提供者方面的任何更改都將破壞契約。
新建BasicMathController,它將發出HTTP請求以從生成的存根中獲取響應:
@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 依賴
對於我們的消費者,我們需要添加spring-cloud-contract-wiremock和spring-cloud-contract-stub-runner依賴項。還有本地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>
存根運行器
現在是時候配置我們的存根運行器,它將通知我們的消費者如何調用我們本地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,模擬服務方。
參數ids定位到maven中的stub.jar。
Ids = groupId : artifactId : version(’+’表示最新版本): 存根 : StubRunner端口
如果你將stub.jar發佈到Maven私服中,可以通過repositoryRoot參數指定私服地址來遠程調用。在測試通過後會根據契約返回響應內容。
總結
文中首先介紹了契約測試的背景以及基於CDC開發服務的大致過程。然後編寫契約文件通過Spring Cloud Contract的contract verifier插件生成存根和服務提供方的測試用例,消費方編寫測試用例,通過StrubRunner模擬服務方來完成一次消費方調用服務方的測試。
本文包含的代碼地址