1、简介 {#1简介}
Spring WebClient 是一款非阻塞、响应式的 HTTP 客户端,而 WireMock 是一个强大的用于模拟基于 HTTP 的 API 的工具。
2、依赖和示例 {#2依赖和示例}
首先,需要在 Spring Boot 项目中添加必要的依赖。
在 pom.xml
中添加 spring-boot-starter-flux (WebClient) 和 spring-cloud-starter-wiremock(WireMock Server)依赖:
<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;
// 构造函数、Getter/Setter 方法省略
我们要使用 WebClient
和 WireMock
进行集成测试,以测试这个功能。
3、使用 WireMock API 进行集成测试 {#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;
// 使用 WireMock baseURL 创建 WebClient 实例 WebClient webClient = webClientBuilder.baseUrl("http://localhost:" + wireMockPort).build();
.... .... }
如上,@AutoConfigureWireMock
会在随机端口上启动一个 WireMock
服务器。通过 WireMock Server 的 base URL 创建了一个 WebClient
实例。
现在,通过 WebClient
发出的任何请求都会转到 WireMock Server 实例,如果存在正确的存根(Stub),就会发送相应的响应。
3.1、存根(Stub)成功的响应以及响应体 {#31存根stub成功的响应以及响应体}
首先,存根(Stub)一个 HTTP 调用(JSON 请求),服务器返回 200 OK。
@Test public void givenWebClientBaseURLConfiguredToWireMock_whenGetRequestForACity_thenWebClientRecievesSuccessResponse() { // 成功检索气象数据的存根(Stub)响应 stubFor(get(urlEqualTo("/weather?city=London")) .willReturn(aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") .withBody("{\"city\": \"London\", \"temperature\": 20, \"description\": \"Cloudy\"}")));
// 使用 WireMock baseURL 创建 WebClient 实例 WebClient webClient = webClientBuilder.baseUrl("http://localhost:" + wireMockPort).build();
// 获取伦敦的天气数据 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
发起 /weather?city=London
请求时,将返回存根(Stub)响应。
3.2、模拟自定义 Header {#32模拟自定义-header}
有时候,HTTP 请求需要自定义 Header。WireMock 可以匹配自定义 Header 以提供相应的响应。
创建一个包含两个 Header 的存根(Stub),一个是 Content-Type
Header,另一个是 X-Custom-Header
标头,其值为 "springdoc-header":
@Test public void givenWebClientBaseURLConfiguredToWireMock_whenGetRequest_theCustomHeaderIsReturned() { // 使用自定义 Heaader 存根(Stub)响应 stubFor(get(urlEqualTo("/weather?city=London")) .willReturn(aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") .withHeader("X-Custom-Header", "springdoc-header") .withBody("{\"city\": \"London\", \"temperature\": 20, \"description\": \"Cloudy\"}")));
//使用 WireMock baseURL 创建 WebClient 实例 WebClient webClient = webClientBuilder.baseUrl("http://localhost:" + wireMockPort).build();
//获取伦敦的天气数据 WeatherData weatherData = webClient.get() .uri("/weather?city=London") .retrieve() .bodyToMono(WeatherData.class) .block();
//断言自定义 header HttpHeaders headers = webClient.get() .uri("/weather?city=London") .exchange() .block() .headers();
assertEquals("springdoc-header", headers.getFirst("X-Custom-Header"));
}
WireMock Server 响应伦敦的存根(Stub)天气数据,包括自定义 Header。
3.3、模拟异常 {#33模拟异常}
另一种测试情况是外部服务返回异常。通过 WireMock Server,可以模拟这些异常情况,查看系统在这些情况下的行为:
@Test public void givenWebClientBaseURLConfiguredToWireMock_whenGetRequestWithInvalidCity_thenExceptionReturnedFromWireMock() { // 无效城市的存根(Stub)响应 stubFor(get(urlEqualTo("/weather?city=InvalidCity")) .willReturn(aResponse() .withStatus(404) .withHeader("Content-Type", "application/json") .withBody("{\"error\": \"City not found\"}")));
//使用 WireMock baseURL 创建 WebClient 实例 WebClient webClient = webClientBuilder.baseUrl("http://localhost:" + wireMockPort).build();
// 获取无效城市的天气数据 WebClientResponseException exception = assertThrows(WebClientResponseException.class, () -> { webClient.get() .uri("/weather?city=InvalidCity") .retrieve() .bodyToMono(WeatherData.class) .block(); });
这里测试的是当查询无效城市的天气数据时,WebClient
是否能正确处理来自服务器的错误响应。验证了在向 /weather?city=InvalidCity
发起请求时是否会抛出 WebClientResponseException
异常,从而确保能在应用中正确处理错误。
3.4、模拟带有查询参数的响应 {#34模拟带有查询参数的响应}
我们经常需要发送带有查询参数的请求。为此创建一个存根(Stub):
@Test public void givenWebClientWithBaseURLConfiguredToWireMock_whenGetWithQueryParameter_thenWireMockReturnsResponse() { // 使用特定查询参数的存根(Stub)响应 stubFor(get(urlPathEqualTo("/weather")) .withQueryParam("city", equalTo("London")) .willReturn(aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") .withBody("{\"city\": \"London\", \"temperature\": 20, \"description\": \"Cloudy\"}")));
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、模拟动态响应 {#35模拟动态响应}
来看一个例子,在响应体中随机生成一个介于 10
度和 30
度之间的温度值:
@Test public void givenWebClientBaseURLConfiguredToWireMock_whenGetRequest_theDynamicResponseIsSent() { stubFor(get(urlEqualTo("/weather?city=London")) .willReturn(aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") .withBody("{\"city\": \"London\", \"temperature\": ${randomValue|10|30}, \"description\": \"Cloudy\"}")));
WebClient webClient = webClientBuilder.baseUrl("http://localhost:" + wireMockPort).build();
WeatherData weatherData = webClient.get() .uri("/weather?city=London") .retrieve() .bodyToMono(WeatherData.class) .block();
//断言温度在预期范围内 assertNotNull(weatherData); assertTrue(weatherData.getTemperature() >= 10 && weatherData.getTemperature() <= 30);
}
3.6、 模拟异步行为 {#36-模拟异步行为}
这里,通过在响应中引入一秒钟的模拟延迟,来模拟现实世界中服务可能会遇到延迟或网络延迟的情况:
@Test public void givenWebClientBaseURLConfiguredToWireMock_whenGetRequest_thenResponseReturnedWithDelay() { stubFor(get(urlEqualTo("/weather?city=London")) .willReturn(aResponse() .withStatus(200) .withFixedDelay(1000) // 1 秒延迟 .withHeader("Content-Type", "application/json") .withBody("{\"city\": \"London\", \"temperature\": 20, \"description\": \"Cloudy\"}")));
WebClient webClient = webClientBuilder.baseUrl("http://localhost:" + wireMockPort).build();
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); // 断言延迟
}
基本上,我们希望确保应用能够优雅地处理延迟响应,而不会超时或遇到意外错误。
3.7、模拟有状态行为 {#37模拟有状态行为}
接下来,结合使用 WireMock 场景来模拟有状态的行为。该 API 允许我们根据状态配置存根(Stub),在多次调用时以不同的方式做出响应:
@Test public void givenWebClientBaseURLConfiguredToWireMock_whenMulitpleGet_thenWireMockReturnsMultipleResponsesBasedOnState() { // 第一个请求的存根(Stub)响应 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"));
// 第二个请求的存根(Stub)响应 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\"}")));
WebClient webClient = webClientBuilder.baseUrl("http://localhost:" + wireMockPort).build();
WeatherData firstWeatherData = webClient.get() .uri("/weather?city=London") .retrieve() .bodyToMono(WeatherData.class) .block();
// 断言第一个响应 assertNotNull(firstWeatherData); assertEquals("London", firstWeatherData.getCity()); assertEquals(20, firstWeatherData.getTemperature()); assertEquals("Cloudy", firstWeatherData.getDescription());
// 再次调用 API WeatherData secondWeatherData = webClient.get() .uri("/weather?city=London") .retrieve() .bodyToMono(WeatherData.class) .block();
// 断言第二个响应 assertNotNull(secondWeatherData); assertEquals("London", secondWeatherData.getCity()); assertEquals(25, secondWeatherData.getTemperature()); assertEquals("Sunny", secondWeatherData.getDescription()); }
如上,在同一个名为 "Weather Scenario" 的场景中为相同的 URL 定义了两个存根(Stub)映射。
当场景是 "started" 状态时,第一个存根(Stub)响应伦敦的天气数据,包括 20°C 的温度和 "Cloudy" 的描述。
响应后,它将场景状态转换为 "Weather Found"。第二个存根(Stub)的配置是,当场景处于 "Weather Found" 状态时,响应温度为 25°C 和描述为 "Weather Found" 的天气数据。
4、总结 {#4总结}
本文介绍了如何使用 Spring WebClient 和 WireMock 进行集成测试,WireMock 为模拟各种场景的 HTTP 响应提供了广泛的存根(Stub)功能。
Ref:https://www.baeldung.com/spring-webclient-wiremock-integration-testing