使用MockWebServer测试Spring WebClient

2023/05/09

1. 概述

之前的文章中,我们介绍了如何使用@RestClientTest测试Spring RestTemplate。

借助这个优雅的解决方案,你可以轻松测试使用RestTemplate的应用程序部分并mock HTTP响应。不幸的是,这个测试设置不适用于Spring WebClient

似乎不会与WebClient的MockRestServiceServer集成,推荐的方法是使用OkHttp的MockWebServer。

在这篇简短的文章中,我们将探讨如何使用MockWebServer来测试使用Spring WebClient的部分应用程序

2. 包含MockWebServer应用程序设置的Spring WebClient

我们的示例项目是一个基本的Spring Boot应用程序。我们将包括Web和WebFlux启动器,因此,Spring Boot会为我们自动配置嵌入式Tomcat,同时我们能够使用Spring WebFlux中的功能,例如WebClient

除了Spring Boot Starter Test之外,该项目还包括来自OkHttp(Java HTTP客户端库)的MockWebServer依赖项:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    <exclusions>
        <exclusion>
            <groupId>org.junit.vintage</groupId>
            <artifactId>junit-vintage-engine</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>mockwebserver</artifactId>
    <version>${mockwebserver.version}</version>
    <scope>test</scope>
</dependency>

将MockWebServer的依赖版本与Spring Boot Starter Parent定义的OkHttp版本对齐非常重要。默认情况下,Spring Boot Parent 2.3引用OkHttp客户端库的版本3.14.8。

在不覆盖OkHttp版本的情况下包含最新版本的MockWebServer会导致以下错误:

java.lang.NoClassDefFoundError: kotlin/TypeCastException

	at cn.tuyucheng.taketoday.UsersClientIntegrationTest.<clinit>(UsersClientTest.java:17)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
Caused by: java.lang.ClassNotFoundException: kotlin.TypeCastException
	at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:581)
	at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178)
	at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:522)
	... 54 more

这就是为什么我们在pom.xml的properties部分中覆盖版本并将两者对齐:

<properties>
    <mockwebserver.version>4.7.2</mockwebserver.version>
    <okhttp3.version>4.7.2</okhttp3.version>
</properties>

最新版本的MockWebServer可以在Maven Central上找到。

3. Spring WebClient用法

Spring WebClient的以下用法对你来说应该很熟悉:

@Component
public class UsersClient {

    private final WebClient webClient;

    public UsersClient(WebClient.Builder builder, @Value("${clients.users.url}") String usersBaseUrl) {
        this.webClient = builder.baseUrl(usersBaseUrl).build();
    }

    public JsonNode getUserById(Long id) {
        return this.webClient
                .get()
                .uri("/users/{id}", id)
                .retrieve()
                .bodyToMono(JsonNode.class)
                .block();
    }
}

此客户端注入自动配置的WebClient.Builder,并使用正确的基本URL配置WebClient。

现在,我们可以将UsersClient注入到我们的应用程序中,并发出HTTP请求来获取用户数据。

为了在生产中使用它,我们可以在application.properties文件中配置实际的URL:

clients.users.url=https://jsonplaceholder.typicode.com/

使此URL可配置非常重要,因为这将帮助我们测试应用程序的这一部分。此外,你还可以从中受益,因为你可以为阶段指定不同的URL。你的沙盒环境可能会使用外部服务的开发端点。

让我们在UsersClient中添加第二个方法,以便稍后测试不同的场景:

public JsonNode createNewUser(JsonNode payload) {
    ClientResponse clientResponse = this.webClient
        .post()
        .uri("/users")
        .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
        .bodyValue(payload)
        .exchange()
        .block();
    
    if (clientResponse.statusCode().equals(HttpStatus.CREATED)) {
        return clientResponse.bodyToMono(JsonNode.class).block();
    } else {
        throw new RuntimeException("Unable to create new user!");
    }
}

此方法确保在验证响应代码时成功创建新用户。如果外部系统返回201(CREATED)以外的任何其他HTTP状态代码,我们将抛出异常。

如果你想了解更多关于Spring WebClient的详细信息,请参考本文

4. 使用MockWebServer的第一个JUnit 5测试

让我们使用MockWebServer编写第一个测试来验证Spring WebClient是否可以检索用户数据。

MockWebServer生成的服务器足够轻量级,我们可以为每个测试方法创建一个服务器。这也确保我们不会在之前的测试中mock HTTP响应而产生任何副作用:

class UsersClientIntegrationTest {

    private MockWebServer mockWebServer;
    private UsersClient usersClient;

    @BeforeEach
    public void setup() throws IOException {
        this.mockWebServer = new MockWebServer();
        this.mockWebServer.start();
        this.usersClient = new UsersClient(WebClient.builder(), mockWebServer.url("/").toString());
    }

    @Test
    public void testGetUserById() throws InterruptedException {
        MockResponse mockResponse = new MockResponse()
                .addHeader("Content-Type", "application/json; charset=utf-8")
                .setBody("{\"id\": 1, \"name\":\"duke\"}")
                .throttleBody(16, 5, TimeUnit.SECONDS);
        mockWebServer.enqueue(mockResponse);

        JsonNode result = usersClient.getUserById(1L);

        assertEquals(1, result.get("id").asInt());
        assertEquals("duke", result.get("name").asText());

        RecordedRequest request = mockWebServer.takeRequest();
        assertEquals("/users/1", request.getPath());
    }
}

JUnit 5的@BeforeEach生命周期方法帮助我们为实际测试准备好一切。除了实例化和启动MockWebServer,我们还将URL传递给我们的UsersClient实例。

对于我们启动的每个新MockWebServer,为我们的被测类(UsersClientIntegrationTest)创建一个新实例很重要,因为mock服务器监听随机的临时端口

在调用我们想要测试的UsersClient方法之前,我们必须准备响应。因此,我们可以构造一个符合我们需求的MockResponse(正文、HTTP标头、响应代码)并使用mockWebServer.enqueue()对其进行排队。

WireMock等其他HTTP响应mock解决方案相比,你可以使用MockWebServer对响应进行排队。本地服务器将按照你对它们进行排队的确切顺序进行响应。这就是为什么我们不指定客户端将访问的实际路径(“/users/1”)并依赖于排队响应的顺序。

作为替代方案,你也可以使用Dispatcher,我们将在下一节中介绍。

最后,MockWebServer提供了一个解决方案来验证我们的应用程序在测试期间发出的HTTP请求。因此,我们可以请求RecordedRequest的实例并断言请求的几个参数(例如路径、标头、有效负载)。如果在测试期间有多个请求,只需调用takeRequest()即可按照请求到达服务器的顺序返回请求

5. 使用Spring WebClient和MockWebServer进行进一步测试

通过第二次测试,我们可以确保我们的客户端能够创建新用户:

@Test
public void testCreatingUsers() {
    MockResponse mockResponse = new MockResponse()
        .addHeader("Content-Type", "application/json; charset=utf-8")
        .setBody("{\"id\": 1, \"name\":\"duke\"}")
        .throttleBody(16, 5, TimeUnit.SECONDS)
        .setResponseCode(201);
    
    mockWebServer.enqueue(mockResponse);
    JsonNode result = usersClient.createNewUser(new ObjectMapper().createObjectNode().put("name", "duke"));
    
    assertEquals(1, result.get("id").asInt());
    assertEquals("duke", result.get("name").asText());
}

设置类似于第一个测试,但这里我们修改服务器的响应代码(默认为200)。

通过此测试,还包含了MockWebServer另一个功能的演示:节流响应。当你想要测试你的应用程序在外部系统缓慢(或过载)的情况下的行为时,这非常有用。

让我们添加第三个测试来验证我们的自定义逻辑(验证响应代码201)是否正常工作。

@Test
public void testCreatingUsersWithNon201ResponseCode() {
    MockResponse mockResponse = new MockResponse()
        .setResponseCode(204);
    mockWebServer.enqueue(mockResponse);

    assertThrows(RuntimeException.class, () -> usersClient.createNewUser(new ObjectMapper().createObjectNode().put("name", "duke")));
}

6. 使用MockWebServer定义不同请求的响应

如果mock响应的顺序不适合你要测试的用例,你可以使用所谓的Dispatcher。

这允许我们根据请求的任何属性(标头、路径、正文等)返回响应。对于大多数情况,URL路径是区分响应的最有用方式:

@Test
public void testMultipleResponseCodes() {
	final Dispatcher dispatcher = new Dispatcher() {
		@Override
		public MockResponse dispatch(RecordedRequest request) {
			return switch (request.getPath()) {
				case "/users/1" -> new MockResponse().setResponseCode(200);
				case "/users/2" -> new MockResponse().setResponseCode(500);
				case "/users/3" -> new MockResponse().setResponseCode(200).setBody("{\"id\": 1, \"name\":\"duke\"}");
				default -> new MockResponse().setResponseCode(404);
			};
		}
	};
    
	mockWebServer.setDispatcher(dispatcher);
	assertThrows(WebClientResponseException.class, () -> usersClient.getUserById(2L));
	assertThrows(WebClientResponseException.class, () -> usersClient.getUserById(4L));
}

在这里,我们为不同的users端点返回不同的响应代码。与使用enqueue()准备响应的其他测试相比,我们在这里使用setDispatcher()并且不排队任何东西。

7. 总结

与我们用于测试RestClient的解决方案相比,设置成本可以忽略不计。

启动MockWebServer速度非常快,直观的API使集成和使用变得简单明了。与WireMock相比,功能集更基本,但对于大多数用例来说已经足够好了。

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章