使用 WireMock 整合測試 Spring WebClient
一、簡介
Spring WebClient 是一個用於執行 HTTP 請求的非阻塞、反應式用戶端,而 WireMock 是一個用於模擬基於 HTTP 的 API 的強大工具。
在本教學中,我們將了解在使用 WebClient 時如何利用 WireMock API 來存根基於 HTTP 的用戶端請求。透過模擬外部服務的行為,我們可以確保我們的應用程式能夠按預期處理和處理外部API回應。
我們將新增所需的依賴項,然後是快速範例。最後,我們將利用 WireMock API 為某些情況編寫一些整合測試。
2. 依賴關係和範例
首先,讓我們確保 Spring Boot 專案中有必要的依賴項。
我們需要用於 WebClient 的[spring-boot-starter-flux](https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-webflux)和用於 WireMock 伺服器的spring-cloud-starter-wiremock .讓我們將它們加入pom.xml:
<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-webflux</artifactId>
 </dependency>
 <dependency>
 <groupId>org.springframework.cloud</groupId>
 <artifactId>spring-cloud-contract-wiremock</artifactId>
 <version>4.1.2</version>
 <scope>test</scope>
 </dependency>現在,讓我們介紹一個簡單的範例,我們將在其中與外部天氣 API 進行通訊以獲取給定城市的天氣資料。接下來我們定義WeatherData POJO:
public class WeatherData {
 private String city;
 private int temperature;
 private String description;
 ....
 //constructor
 //setters and getters
 }
我們希望使用 WebClient 和 WireMock 進行整合測試來測試此功能。
3. 使用 WireMock API 進行整合測試
讓我們先使用WireMock和WebClient設定 Spring Boot 測試類別:
@RunWith(SpringRunner.class)
 @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
 @AutoConfigureWireMock(port = 0)
 public class WeatherServiceIntegrationTest {
 @Autowired
 private WebClient.Builder webClientBuilder;
 @Value("${wiremock.server.port}")
 private int wireMockPort;
 // Create WebClient instance with WireMock base URL
 WebClient webClient = webClientBuilder.baseUrl("http://localhost:" + wireMockPort).build();
 ....
 ....
 }值得注意的是,@ AutoConfigureWireMock會在隨機連接埠上自動啟動 WireMock 伺服器。此外,我們正在使用 WireMock 伺服器的基本 URL 建立一個WebClient實例。現在,透過webClient的任何請求都會傳送到 WireMock 伺服器實例,如果存在正確的存根,則會傳送適當的回應。
3.1.使用成功和 JSON 正文存根回應
讓我們先使用 JSON 請求來存根 HTTP 調用,並讓伺服器返回200 OK :
@Test
 public void givenWebClientBaseURLConfiguredToWireMock_whenGetRequestForACity_thenWebClientRecievesSuccessResponse() {
 // Stubbing response for a successful weather data retrieval
 stubFor(get(urlEqualTo("/weather?city=London"))
 .willReturn(aResponse()
 .withStatus(200)
 .withHeader("Content-Type", "application/json")
 .withBody("{\"city\": \"London\", \"temperature\": 20, \"description\": \"Cloudy\"}")));
 // Create WebClient instance with WireMock base URL
 WebClient webClient = webClientBuilder.baseUrl("http://localhost:" + wireMockPort).build();
 // Fetch weather data for London
 WeatherData weatherData = webClient.get()
 .uri("/weather?city=London")
 .retrieve()
 .bodyToMono(WeatherData.class)
 .block();
 assertNotNull(weatherData);
 assertEquals("London", weatherData.getCity());
 assertEquals(20, weatherData.getTemperature());
 assertEquals("Cloudy", weatherData.getDescription());
 }當透過WebClient發送帶有指向 WireMock 連接埠的基本 URL 的/weather?city=London請求時,將返回存根回應,然後根據需要在我們的系統中使用該回應。
3.2.模擬自訂標頭
有時 HTTP 請求需要自訂標頭。 WireMock可以匹配自訂標頭以提供適當的回應。
讓我們建立一個包含兩個標頭的存根,一個是Content-Type ,另一個是X-Custom-Header ,其值為“baeldung-header” :
@Test
 public void givenWebClientBaseURLConfiguredToWireMock_whenGetRequest_theCustomHeaderIsReturned() {
 //Stubbing response with custom headers
 stubFor(get(urlEqualTo("/weather?city=London"))
 .willReturn(aResponse()
 .withStatus(200)
 .withHeader("Content-Type", "application/json")
 .withHeader("X-Custom-Header", "baeldung-header")
 .withBody("{\"city\": \"London\", \"temperature\": 20, \"description\": \"Cloudy\"}")));
 //Create WebClient instance with WireMock base URL
 WebClient webClient = webClientBuilder.baseUrl("http://localhost:" + wireMockPort).build();
 //Fetch weather data for London
 WeatherData weatherData = webClient.get()
 .uri("/weather?city=London")
 .retrieve()
 .bodyToMono(WeatherData.class)
 .block();
 //Assert the custom header
 HttpHeaders headers = webClient.get()
 .uri("/weather?city=London")
 .exchange()
 .block()
 .headers();
 assertEquals("baeldung-header", headers.getFirst("X-Custom-Header"));
 }WireMock伺服器使用倫敦的存根天氣資料回應,包括自訂標頭。
3.3.模擬異常
另一個有用的測試案例是外部服務回傳異常時。 WireMock 伺服器可讓我們模擬這些特殊場景,以查看在這種情況下我們的系統行為:
@Test
 public void givenWebClientBaseURLConfiguredToWireMock_whenGetRequestWithInvalidCity_thenExceptionReturnedFromWireMock() {
 //Stubbing response for an invalid city
 stubFor(get(urlEqualTo("/weather?city=InvalidCity"))
 .willReturn(aResponse()
 .withStatus(404)
 .withHeader("Content-Type", "application/json")
 .withBody("{\"error\": \"City not found\"}")));
 // Create WebClient instance with WireMock base URL
 WebClient webClient = webClientBuilder.baseUrl("http://localhost:" + wireMockPort).build();
 // Fetch weather data for an invalid city
 WebClientResponseException exception = assertThrows(WebClientResponseException.class, () -> {
 webClient.get()
 .uri("/weather?city=InvalidCity")
 .retrieve()
 .bodyToMono(WeatherData.class)
 .block();
 });重要的是,我們在這裡測試WebClient在查詢無效城市的天氣資料時是否正確處理來自伺服器的錯誤回應。它驗證在向/weather?city=InvalidCity發出請求時是否引發WebClientResponseException ,從而確保應用程式中正確處理錯誤。
3.4.使用查詢參數模擬回應
通常,我們必須傳送帶有查詢參數的請求。接下來讓我們為此建立一個存根:
@Test
 public void givenWebClientWithBaseURLConfiguredToWireMock_whenGetWithQueryParameter_thenWireMockReturnsResponse() {
 // Stubbing response with specific query parameters
 stubFor(get(urlPathEqualTo("/weather"))
 .withQueryParam("city", equalTo("London"))
 .willReturn(aResponse()
 .withStatus(200)
 .withHeader("Content-Type", "application/json")
 .withBody("{\"city\": \"London\", \"temperature\": 20, \"description\": \"Cloudy\"}")));
 //Create WebClient instance with WireMock base URL
 WebClient webClient = webClientBuilder.baseUrl("http://localhost:" + wireMockPort).build();
 WeatherData londonWeatherData = webClient.get()
 .uri(uriBuilder -> uriBuilder.path("/weather").queryParam("city", "London").build())
 .retrieve()
 .bodyToMono(WeatherData.class)
 .block();
 assertEquals("London", londonWeatherData.getCity());
 }3.5.模擬動態響應
讓我們來看一個在響應主體中產生10到30度之間的隨機溫度值的範例:
@Test
 public void givenWebClientBaseURLConfiguredToWireMock_whenGetRequest_theDynamicResponseIsSent() {
 //Stubbing response with dynamic temperature
 stubFor(get(urlEqualTo("/weather?city=London"))
 .willReturn(aResponse()
 .withStatus(200)
 .withHeader("Content-Type", "application/json")
 .withBody("{\"city\": \"London\", \"temperature\": ${randomValue|10|30}, \"description\": \"Cloudy\"}")));
 //Create WebClient instance with WireMock base URL
 WebClient webClient = webClientBuilder.baseUrl("http://localhost:" + wireMockPort).build();
 //Fetch weather data for London
 WeatherData weatherData = webClient.get()
 .uri("/weather?city=London")
 .retrieve()
 .bodyToMono(WeatherData.class)
 .block();
 //Assert temperature is within the expected range
 assertNotNull(weatherData);
 assertTrue(weatherData.getTemperature() >= 10 && weatherData.getTemperature() <= 30);
 }3.6.模擬異步行為
在這裡,我們將嘗試透過在回應中引入一秒鐘的模擬延遲來模擬服務可能遇到延遲或網路延遲的現實場景:
@Test
 public void givenWebClientBaseURLConfiguredToWireMock_whenGetRequest_thenResponseReturnedWithDelay() {
 //Stubbing response with a delay
 stubFor(get(urlEqualTo("/weather?city=London"))
 .willReturn(aResponse()
 .withStatus(200)
 .withFixedDelay(1000) // 1 second delay
 .withHeader("Content-Type", "application/json")
 .withBody("{\"city\": \"London\", \"temperature\": 20, \"description\": \"Cloudy\"}")));
 //Create WebClient instance with WireMock base URL
 WebClient webClient = webClientBuilder.baseUrl("http://localhost:" + wireMockPort).build();
 //Fetch weather data for London
 long startTime = System.currentTimeMillis();
 WeatherData weatherData = webClient.get()
 .uri("/weather?city=London")
 .retrieve()
 .bodyToMono(WeatherData.class)
 .block();
 long endTime = System.currentTimeMillis();
 assertNotNull(weatherData);
 assertTrue(endTime - startTime >= 1000); // Assert the delay
 }本質上,我們希望確保應用程式優雅地處理延遲回應,而不會逾時或遇到意外錯誤。
3.7.模擬有狀態行為
接下來,我們結合使用WireMock場景來模擬有狀態行為。 API 允許我們將存根配置為在多次呼叫時根據狀態做出不同的回應:
@Test
 public void givenWebClientBaseURLConfiguredToWireMock_whenMulitpleGet_thenWireMockReturnsMultipleResponsesBasedOnState() {
 //Stubbing response for the first call
 stubFor(get(urlEqualTo("/weather?city=London"))
 .inScenario("Weather Scenario")
 .whenScenarioStateIs("started")
 .willReturn(aResponse()
 .withStatus(200)
 .withHeader("Content-Type", "application/json")
 .withBody("{\"city\": \"London\", \"temperature\": 20, \"description\": \"Cloudy\"}"))
 .willSetStateTo("Weather Found"));
 // Stubbing response for the second call
 stubFor(get(urlEqualTo("/weather?city=London"))
 .inScenario("Weather Scenario")
 .whenScenarioStateIs("Weather Found")
 .willReturn(aResponse()
 .withStatus(200)
 .withHeader("Content-Type", "application/json")
 .withBody("{\"city\": \"London\", \"temperature\": 25, \"description\": \"Sunny\"}")));
 //Create WebClient instance with WireMock base URL
 WebClient webClient = webClientBuilder.baseUrl("http://localhost:" + wireMockPort).build();
 //Fetch weather data for London
 WeatherData firstWeatherData = webClient.get()
 .uri("/weather?city=London")
 .retrieve()
 .bodyToMono(WeatherData.class)
 .block();
 //Assert the first response
 assertNotNull(firstWeatherData);
 assertEquals("London", firstWeatherData.getCity());
 assertEquals(20, firstWeatherData.getTemperature());
 assertEquals("Cloudy", firstWeatherData.getDescription());
 // Fetch weather data for London again
 WeatherData secondWeatherData = webClient.get()
 .uri("/weather?city=London")
 .retrieve()
 .bodyToMono(WeatherData.class)
 .block();
 // Assert the second response
 assertNotNull(secondWeatherData);
 assertEquals("London", secondWeatherData.getCity());
 assertEquals(25, secondWeatherData.getTemperature());
 assertEquals("Sunny", secondWeatherData.getDescription());
 }本質上,我們在名為「 Weather Scenario 」的同一場景中為同一 URL 定義了兩個存根映射。然而,我們已經配置了第一個存根,以在場景處於「 started 」狀態時響應倫敦的天氣數據,溫度為20 °C,描述為「 Cloudy 」。
回應後,它將場景狀態轉換為「 Weather Found 」。第二個存根配置為當場景處於「 Weather Found 」狀態時,以溫度為25 °C 且描述為「 Sunny 」的不同天氣資料回應。
4。
在本文中,我們討論了使用 Spring WebClient 和 WireMock 進行整合測試的基礎知識。 WireMock 提供了豐富的功能來存根 HTTP 回應以模擬各種場景。
我們快速地查看了一些經常遇到的結合 WebClient 來模擬 HTTP 回應的場景。
與往常一樣,本文的完整實作可以在 GitHub 上找到。