1. 简介
Spring WebClient 是一个非阻塞的响应式 HTTP 客户端,而 WireMock 则是模拟 HTTP API 的强大工具。本文将介绍如何利用 WireMock 来模拟 WebClient 的 HTTP 请求,通过模拟外部服务行为,确保我们的应用能正确处理外部 API 响应。
我们将从添加依赖开始,通过一个简单示例快速上手,最后使用 WireMock API 编写多种场景的集成测试。
2. 依赖与示例
首先确保 Spring Boot 项目包含必要依赖。需要 spring-boot-starter-webflux
(提供 WebClient)和 spring-cloud-contract-wiremock
(提供 WireMock 服务器):
<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 进行集成测试
3.1. 测试类基础配置
先搭建 Spring Boot 测试类,集成 WireMock 和 WebClient:
@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 服务器的 WebClient
WebClient webClient = webClientBuilder.baseUrl("http://localhost:" + wireMockPort).build();
....
....
}
⚠️ 注意:@AutoConfigureWireMock
会自动在随机端口启动 WireMock 服务器。我们创建的 WebClient 实例将所有请求导向该服务器,只要存在匹配的 stub,就能获得预设响应。
3.2. 模拟成功 JSON 响应
先模拟返回 200 状态码和 JSON 响应的 HTTP 调用:
@Test
public void givenWebClientBaseURLConfiguredToWireMock_whenGetRequestForACity_thenWebClientRecievesSuccessResponse() {
// 模拟成功获取天气数据的响应
stubFor(get(urlEqualTo("/weather?city=London"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("{\"city\": \"London\", \"temperature\": 20, \"description\": \"Cloudy\"}")));
// 创建指向 WireMock 的 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 通过指向 WireMock 端口的 base URL 请求 /weather?city=London
时,将返回预设的模拟响应。
3.3. 模拟自定义请求头
有时 HTTP 请求需要自定义头信息。WireMock 可以根据请求头匹配返回对应响应:
@Test
public void givenWebClientBaseURLConfiguredToWireMock_whenGetRequest_theCustomHeaderIsReturned() {
// 模拟包含自定义头的响应
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\"}")));
// 创建指向 WireMock 的 WebClient
WebClient webClient = webClientBuilder.baseUrl("http://localhost:" + wireMockPort).build();
// 获取伦敦天气数据
WeatherData weatherData = webClient.get()
.uri("/weather?city=London")
.retrieve()
.bodyToMono(WeatherData.class)
.block();
// 验证自定义头
HttpHeaders headers = webClient.get()
.uri("/weather?city=London")
.exchange()
.block()
.headers();
assertEquals("baeldung-header", headers.getFirst("X-Custom-Header"));
}
WireMock 服务器返回包含自定义头的模拟天气数据。
3.4. 模拟异常响应
测试外部服务返回异常的场景同样重要。WireMock 可模拟这些异常情况:
@Test
public void givenWebClientBaseURLConfiguredToWireMock_whenGetRequestWithInvalidCity_thenExceptionReturnedFromWireMock() {
// 模拟无效城市的响应
stubFor(get(urlEqualTo("/weather?city=InvalidCity"))
.willReturn(aResponse()
.withStatus(404)
.withHeader("Content-Type", "application/json")
.withBody("{\"error\": \"City not found\"}")));
// 创建指向 WireMock 的 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.5. 模拟带查询参数的响应
实际开发中常需发送带查询参数的请求。下面模拟这种场景:
@Test
public void givenWebClientWithBaseURLConfiguredToWireMock_whenGetWithQueryParameter_thenWireMockReturnsResponse() {
// 模拟特定查询参数的响应
stubFor(get(urlPathEqualTo("/weather"))
.withQueryParam("city", equalTo("London"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("{\"city\": \"London\", \"temperature\": 20, \"description\": \"Cloudy\"}")));
// 创建指向 WireMock 的 WebClient
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.6. 模拟动态响应
下面模拟返回随机温度值(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\"}")));
// 创建指向 WireMock 的 WebClient
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.7. 模拟异步行为
通过模拟 1 秒延迟响应,测试应用处理延迟的能力:
@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\"}")));
// 创建指向 WireMock 的 WebClient
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.8. 模拟有状态行为
使用 WireMock 场景(Scenario)模拟有状态行为,使同一接口在不同调用时返回不同响应:
@Test
public void givenWebClientBaseURLConfiguredToWireMock_whenMulitpleGet_thenWireMockReturnsMultipleResponsesBasedOnState() {
// 模拟首次调用的响应
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"));
// 模拟第二次调用的响应
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\"}")));
// 创建指向 WireMock 的 WebClient
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());
// 再次获取伦敦天气
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" 场景中定义两个 stub:
- 首次调用(状态为 "started")返回 20℃ 的多云天气,并将状态转为 "Weather Found"
- 第二次调用(状态为 "Weather Found")返回 25℃ 的晴天
4. 总结
本文介绍了使用 Spring WebClient 和 WireMock 进行集成测试的基础知识。WireMock 提供了强大的 HTTP 响应模拟能力,可覆盖多种测试场景。
我们快速实践了 WebClient 结合 WireMock 的常见测试用例:
- ✅ 成功 JSON 响应
- ✅ 自定义请求头匹配
- ✅ 异常响应处理
- ✅ 查询参数匹配
- ✅ 动态响应生成
- ✅ 异步延迟模拟
- ✅ 有状态行为测试
完整代码实现可在 GitHub 查看。