1. 簡介
Spring Cloud Contract 簡單來説,幫助我們編寫 基於消費者的合同 (CDC)。
這確保了分佈式系統中的 生產者 和 消費者 之間的合同——無論是以 HTTP 還是基於消息的方式進行交互。
在本文中,我們將通過 HTTP 交互來探索編寫 Spring Cloud Contract 上的生產者和消費者端測試用例。
2. 服務器端生產者
我們將編寫一個服務器端 CDC,形式為 EvenOddController – 它僅根據 number 參數判斷其是否為偶數或奇數:
@RestController
public class EvenOddController {
@GetMapping("/validate/prime-number")
public String isNumberPrime(@RequestParam("number") Integer number) {
return Integer.parseInt(number) % 2 == 0 ? "Even" : "Odd";
}
}2.1 Maven 依賴
對於我們的生產者端,我們需要以下依賴項:spring-cloud-starter-contract-verifier。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-verifier</artifactId>
<version>2.1.1.RELEASE</version>
<scope>test</scope>
</dependency>我們還需要配置 spring-cloud-contract-maven-plugin,具體名稱將我們在下一部分中進行説明。
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>2.1.1.RELEASE</version>
<extensions>true</extensions>
<configuration>
<baseClassForTests>
com.baeldung.spring.cloud.springcloudcontractproducer.BaseTestClass
</baseClassForTests>
</configuration>
</plugin>2.2. 生產者端設置
我們需要在測試包中添加一個基礎類,該類加載我們的 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/ 包中,我們將添加測試樁,例如在 shouldReturnEvenWhenRequestParamIsEven.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
}
}
當運行構建時,插件會自動生成一個名為 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");
}
構建還會將佔位符 JAR 文件添加到我們的本地 Maven 倉庫中,以便由我們的消費者使用。
佔位符文件將位於輸出文件夾下的 stubs/mapping/。
3. 消費者 – 客户端側
我們的 CDC 的客户端側將通過 HTTP 交互消耗生產者側生成的 stub,以維護合同,因此 生產者側的任何變更都將破壞合同。
我們將添加 BasicMathController,它將通過 HTTP 請求獲取來自生成的 stub 的響應:
@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();
}
}3.1 Maven 依賴
為了我們的消費者,我們需要添加 <a href="https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-contract-wiremock">spring-cloud-contract-wiremock</a> 和 <a href="https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-contract-stub-runner">spring-cloud-contract-stub-runner</a> 依賴。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-wiremock</artifactId>
<version>2.1.1.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-stub-runner</artifactId>
<version>2.1.1.RELEASE</version>
<scope>test</scope>
</dependency>
3.2. 消費者端配置
現在是時候配置我們的 stub 運行器,它將告知消費者在本地 Maven 倉庫中可用的 stub:
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureMockMvc
@AutoConfigureJsonTesters
@AutoConfigureStubRunner(
stubsMode = StubRunnerProperties.StubsMode.LOCAL,
ids = "com.baeldung.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"));
}
}請注意,ids 屬性的 @AutoConfigureStubRunner 註解指定了:
- com.baeldung.spring.cloud — 我們的 Artifact 的 groupId
- spring-cloud-contract-producer — 生產者 Stub JAR 的 artifactId
- 8090 — 生成的 Stub 將運行的端口
4. 合同違約情況
如果我們在生產者端進行任何直接影響合同且未同時更新消費者端的更改,這可能導致合同失敗。
例如,假設我們更改生產者端 EvenOddController 請求 URI 為 /validate/change/prime-number。
如果我們未能告知消費者這一變更,消費者仍將向 /validate/prime-number URI 發送請求,消費者端測試用例將拋出 org.springframework.web.client.HttpClientErrorException: 404 Not Found 異常。
5. 總結
我們已經瞭解了 Spring Cloud Contract 如何幫助我們維護消費者和生產者之間的合同,從而在不擔心違反合同的情況下發布新代碼。