51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

使用 Spring WebClient 和 WireMock 进行集成测试

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 方法省略

我们要使用 WebClientWireMock 进行集成测试,以测试这个功能。

3、使用 WireMock API 进行集成测试 {#3使用-wiremock-api-进行集成测试}

首先用 WireMockWebClient 设置 Spring Boot 测试类:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureWireMock(port = 0)
public class WeatherServiceIntegrationTest {
@Autowired
private WebClient.Builder webClientBuilder;

@Value(&quot;${wiremock.server.port}&quot;) private int wireMockPort;

// 使用 WireMock baseURL 创建 WebClient 实例 WebClient webClient = webClientBuilder.baseUrl(&quot;http://localhost:&quot; + 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(&quot;http://localhost:&quot; + wireMockPort).build();

// 获取伦敦的天气数据 WeatherData weatherData = webClient.get() .uri(&quot;/weather?city=London&quot;) .retrieve() .bodyToMono(WeatherData.class) .block(); assertNotNull(weatherData); assertEquals(&quot;London&quot;, weatherData.getCity()); assertEquals(20, weatherData.getTemperature()); assertEquals(&quot;Cloudy&quot;, 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(&quot;http://localhost:&quot; + wireMockPort).build();

//获取伦敦的天气数据 WeatherData weatherData = webClient.get() .uri(&quot;/weather?city=London&quot;) .retrieve() .bodyToMono(WeatherData.class) .block();

//断言自定义 header HttpHeaders headers = webClient.get() .uri(&quot;/weather?city=London&quot;) .exchange() .block() .headers();
assertEquals(&quot;springdoc-header&quot;, headers.getFirst(&quot;X-Custom-Header&quot;));

}

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(&quot;http://localhost:&quot; + wireMockPort).build();

WeatherData londonWeatherData = webClient.get() .uri(uriBuilder -&gt; uriBuilder.path(&quot;/weather&quot;).queryParam(&quot;city&quot;, &quot;London&quot;).build()) .retrieve() .bodyToMono(WeatherData.class) .block(); assertEquals(&quot;London&quot;, 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(&quot;http://localhost:&quot; + wireMockPort).build();

WeatherData weatherData = webClient.get() .uri(&quot;/weather?city=London&quot;) .retrieve() .bodyToMono(WeatherData.class) .block();

//断言温度在预期范围内 assertNotNull(weatherData); assertTrue(weatherData.getTemperature() &gt;= 10 &amp;&amp; weatherData.getTemperature() &lt;= 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(&quot;http://localhost:&quot; + wireMockPort).build();

long startTime = System.currentTimeMillis(); WeatherData weatherData = webClient.get() .uri(&quot;/weather?city=London&quot;) .retrieve() .bodyToMono(WeatherData.class) .block(); long endTime = System.currentTimeMillis();

assertNotNull(weatherData); assertTrue(endTime - startTime &gt;= 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(&quot;/weather?city=London&quot;))
  .inScenario(&quot;Weather Scenario&quot;)
  .whenScenarioStateIs(&quot;Weather Found&quot;)
  .willReturn(aResponse()
    .withStatus(200)
    .withHeader(&quot;Content-Type&quot;, &quot;application/json&quot;)
    .withBody(&quot;{\&quot;city\&quot;: \&quot;London\&quot;, \&quot;temperature\&quot;: 25, \&quot;description\&quot;: \&quot;Sunny\&quot;}&quot;)));

WebClient webClient = webClientBuilder.baseUrl(&quot;http://localhost:&quot; + wireMockPort).build();

WeatherData firstWeatherData = webClient.get() .uri(&quot;/weather?city=London&quot;) .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

赞(4)
未经允许不得转载:工具盒子 » 使用 Spring WebClient 和 WireMock 进行集成测试