使用 Spring WebClient 和 WireMock 进行集成测试
1、简介
Spring WebClient 是一款非阻塞、响应式的 HTTP 客户端,而 WireMock 是一个强大的用于模拟基于 HTTP 的 API 的工具。
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 进行集成测试
首先用 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)成功的响应以及响应体
首先,存根(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
有时候,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、模拟异常
另一种测试情况是外部服务返回异常。通过 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、模拟带有查询参数的响应
我们经常需要发送带有查询参数的请求。为此创建一个存根(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、模拟动态响应
来看一个例子,在响应体中随机生成一个介于 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、 模拟异步行为
这里,通过在响应中引入一秒钟的模拟延迟,来模拟现实世界中服务可能会遇到延迟或网络延迟的情况:
@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、模拟有状态行为
接下来,结合使用 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、总结
本文介绍了如何使用 Spring WebClient 和 WireMock 进行集成测试,WireMock 为模拟各种场景的 HTTP 响应提供了广泛的存根(Stub)功能。
Ref:https://www.baeldung.com/spring-webclient-wiremock-integration-testing