In one of the last blog post, I demonstrated how to test the Spring RestTemplate with @RestClientTest. With this elegant solution, you can easily test parts of your application that use the RestTemplate
and mock HTTP responses. Unfortunately, this test setup does not work for the Spring WebClient
. It seems there won't be an integration with the MockRestServiceServer
for the WebClient
. The recommended way is to use MockWebServer from OkHttp. With this blog post, you'll learn how to use the MockWebServer to test parts of your application using the Spring WebClient.
Spring WebClient with MockWebServer application setup
Our sample project is a basic Spring Boot application. We'll include both starters for Web and WebFlux. Hence Spring Boot autoconfigures the embedded Tomcat for us while we are able to use parts from Spring WebFlux like the WebClient.
Besides the Spring Boot Start Test, the project includes the MockWebServer dependency from OkHttp (Java HTTP Client library):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.0.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>de.rieckpil.blog</groupId> <artifactId>spring-web-client-testing-with-mockwebserver</artifactId> <version>0.0.1-SNAPSHOT</version> <name>spring-web-client-testing-with-mockwebserver</name> <description>Demo project for Spring Boot</description> <properties> <java.version>11</java.version> <mockwebserver.version>4.7.2</mockwebserver.version> <okhttp3.version>4.7.2</okhttp3.version> </properties> <dependencies> <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> </dependencies> <!-- standard build section --> </project> |
It's important to align the dependency version of the MockWebServer with the defined version of OkHttp by the Spring Boot Starter Parent. By default the Spring Boot 2.3 Parent references version 3.14.8 of the OkHttp client library.
Including a recent version of MockWebServer without overriding the OkHttp version, results in the following error:
1 2 3 4 5 6 7 8 9 10 11 12 | java.lang.NoClassDefFoundError: kotlin/TypeCastException at de.rieckpil.blog.UsersClientTest.<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 |
That's why we override the version inside the properties
section of our pom.xml
and align both:
1 2 3 4 5 | <properties> <java.version>11</java.version> <mockwebserver.version>4.7.2</mockwebserver.version> <okhttp3.version>4.7.2</okhttp3.version> </properties> |
Spring WebClient usage
The following usage of the Spring WebClient
should look familiar to you:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | @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(); } } |
This client injects the autoconfigured WebClient.Builder
and configures a WebClient
with the correct base URL. We can now inject the UsersClient
inside our application and make HTTP requests to fetch user data.
For using this in production, we can configure the actual URL inside our application.properties
file:
1 | clients.users.url=https://jsonplaceholder.typicode.com/ |
It's important to make this URL configurable, as this will help us to test this part of the application. Furthermore, you also benefit as you can specify different URLs for your stages. Your sandbox environment might use a development endpoint of the external service.
Let's add a second method inside the UsersClient
to test different scenarios later on:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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!"); } } |
This method ensures to successfully create a new user while verifying the response code. In case the external system returns any other HTTP status code than 201 (created), we'll throw an exception.
If you are new to the Spring WebClient
and wonder why you should use it instead of the good old RestTemplate
, consider reading this guide.
First JUnit 5 test with the MockWebServer
Let's write the first test using MockWebServer
to verify the Spring WebClient
can retrieve user data. The spawned server by MockWebServer
is lightweight enough that we can create one server for each test method. This also ensures we won't have any side-effects from mocking HTTP responses in previous tests:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | class UsersClientTest { 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()); } } |
The @BeforeEach
lifecycle method from JUnit 5 helps us to prepare everything for our actual test. Besides instantiating and starting the MockWebServer
, we pass the URL to our UsersClient
instance. It's important to create a new instance for our class under test (UsersClient
here) for each new MockWebServer we start, as the mock server listens on a random ephemeral port.
Before we invoke the method of the UsersClient
that we want to test, we have to prepare the response. Therefore we can construct a MockResponse
matching our needs (body, HTTP header, response code) and queue it using mockWebServer.enqueue()
.
Compared to other HTTP response mocking solutions like WireMock, you queue your responses with MockWebServer
. The local server will respond with the in the exact order you queue them. That's why we don't specify the actual path ("/users/1"
) our client will access and rely on the order of enqueued responses.
As an alternative, you can also use a Dispatcher
, which we'll cover in of the next sections.
Finally, the MockWebServer
provides a solution to verify the HTTP requests our application made during the test. Therefore we can request an instance of RecordedRequest
and assert several parameters of the request (e.g. path, header, payload). If there are multiple requests during a test, simply invoke .takeRequest()
to return the requests in the order they arrived at the server.
Further testing with Spring WebClient and MockWebServer
With a second test, we can ensure our client is able to create new users:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | @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()); } |
The setup is similar to the first test, but here we modify the response code of the server (default is 200).
With this test, I've also included a demo for another feature of MockWebServer
: throttling responses. This is quite helpful when you want to test how your application behaves in case of a slow (or overloaded) external system.
Let's add a third test to verify our custom logic (verifying response code 201) is working.
1 2 3 4 5 6 7 8 9 10 | @Test public void testCreatingUsersWithNon201ResponseCode() { MockResponse mockResponse = new MockResponse() .setResponseCode(204); mockWebServer.enqueue(mockResponse); assertThrows(RuntimeException.class, () -> usersClient.createNewUser(new ObjectMapper().createObjectNode().put("name", "duke"))); } |
Define responses for different requests with MockWebServer
In case the sequential ordering of mocked responses does not fit the use case you want to test, you can use a so-called Dispatcher
.
This allows us to return responses based on any attribute of the request (header, path, body, etc.). For most of the cases the URL path is the most useful way to differentiate responses:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | @Test public void testMultipleResponseCodes() { final Dispatcher dispatcher = new Dispatcher() { @Override public MockResponse dispatch(RecordedRequest request) { switch (request.getPath()) { case "/users/1": return new MockResponse().setResponseCode(200); case "/users/2": return new MockResponse().setResponseCode(500); case "/users/3": return new MockResponse().setResponseCode(200).setBody("{\"id\": 1, \"name\":\"duke\"}"); } return new MockResponse().setResponseCode(404); } }; mockWebServer.setDispatcher(dispatcher); assertThrows(WebClientResponseException.class, () -> usersClient.getUserById(2L)); assertThrows(WebClientResponseException.class, () -> usersClient.getUserById(4L)); } |
Here we return different response codes for different user endpoints. Compared to the other tests where we used .enqueue()
to prepare the response, we use .setDispatcher()
here and don't enqueue anything.
Summary of using MockWebServer for testing Spring WebClient
With this blog post, you should now be able to test the Spring WebClient parts of your application with MockWebServer
.
The setup costs compared to the solution we have for testing the RestClient is negligible. Starting the MockWebServer
is quite fast and the intuitive API makes the integration and usage straightforward. Compared to WireMock the feature set is more basic but good enough for most use cases.
You can find the sample application for this blog post on GitHub.
For more Spring WebClient related content, follow the links below:
- Spring WebTestClient for efficient testing of your REST API
- Use Spring WebClient for RESTful communication
- Spring WebClient OAuth2 Integration for Spring WebFlux
- Expose Metrics of Spring WebClient using Spring Boot Actuator
Have fun testing your Spring WebClient with MockWebServer,
Phil