1. Overview
These days, we expect to call REST APIs in most of our services. Spring provides a few options for building a REST client, and WebClient is recommended.
In this quick tutorial, we will look at how to unit test services that use WebClient to call APIs.
2. Mocking
We have two main options for mocking in our tests:
- Use Mockito to mimic the behavior of WebClient
- Use WebClient for real, but mock the service it calls by using MockWebServer (okhttp)
3. Using Mockito
Mockito is the most common mocking library for Java. It's good at providing pre-defined responses to method calls, but things get challenging when mocking fluent APIs. This is because in a fluent API, a lot of objects pass between the calling code and the mock.
For example, let's have an EmployeeService class with a getEmployeeById method to fetch data via HTTP using WebClient:
public class EmployeeService { public Mono<Employee> getEmployeeById(Integer employeeId) { return webClient .get() .uri("http://localhost:8080/employee/{id}", employeeId) .retrieve() .bodyToMono(Employee.class); } }
We can use Mockito to mock this:
@ExtendWith(MockitoExtension.class) public class EmployeeServiceTest { @Test void givenEmployeeId_whenGetEmployeeById_thenReturnEmployee() { Integer employeeId = 100; Employee mockEmployee = new Employee(100, "Adam", "Sandler", 32, Role.LEAD_ENGINEER); when(webClientMock.get()) .thenReturn(requestHeadersUriSpecMock); when(requestHeadersUriMock.uri("/employee/{id}", employeeId)) .thenReturn(requestHeadersSpecMock); when(requestHeadersMock.retrieve()) .thenReturn(responseSpecMock); when(responseMock.bodyToMono(Employee.class)) .thenReturn(Mono.just(mockEmployee)); Mono<Employee> employeeMono = employeeService.getEmployeeById(employeeId); StepVerifier.create(employeeMono) .expectNextMatches(employee -> employee.getRole() .equals(Role.LEAD_ENGINEER)) .verifyComplete(); } }
As we can see, we need to provide a different mock object for each call in the chain, with four different when/thenReturn calls required. This is verbose and cumbersome. It also requires us to know the implementation details of how exactly our service uses WebClient, making this a brittle way of testing.
How can we write better tests for WebClient?
4. Using MockWebServer
MockWebServer, built by the Square team, is a small web server that can receive and respond to HTTP requests.
Interacting with MockWebServer from our test cases allows our code to use real HTTP calls to a local endpoint. We get the benefit of testing the intended HTTP interactions and none of the challenges of mocking a complex fluent client.
Using MockWebServer is recommended by the Spring Team for writing integration tests.
4.1. MockWebServer Dependencies
To use MockWebServer, we need to add Maven dependencies for both okhttp and mockwebserver to our pom.xml:
<dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> <version>4.0.1</version> <scope>test</scope> </dependency> <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>mockwebserver</artifactId> <version>4.0.1</version> <scope>test</scope> </dependency>
4.2. Adding MockWebServer to our Test
Let's test our EmployeeService with MockWebServer:
public class EmployeeServiceMockWebServerTest { public static MockWebServer mockBackEnd; @BeforeAll static void setUp() throws IOException { mockBackEnd = new MockWebServer(); mockBackEnd.start(); } @AfterAll static void tearDown() throws IOException { mockBackEnd.shutdown(); } }
In the above JUnit Test class, the setUp and tearDown method takes care of creating and shutting down the MockWebServer.
The next step is to map the port of the actual REST service call to the MockWebServer's port.
@BeforeEach void initialize() { String baseUrl = String.format("http://localhost:%s", mockBackEnd.getPort()); employeeService = new EmployeeService(baseUrl); }
Now it's time to create a stub so that the MockWebServer can respond to an HttpRequest.
4.3. Stubbing a Response
Let's use MockWebServer's handy enqueue method to queue a test response on the web server:
@Test void getEmployeeById() throws Exception { Employee mockEmployee = new Employee(100, "Adam", "Sandler", 32, Role.LEAD_ENGINEER); mockBackEnd.enqueue(new MockResponse() .setBody(objectMapper.writeValueAsString(mockEmployee)) .addHeader("Content-Type", "application/json")); Mono<Employee> employeeMono = employeeService.getEmployeeById(100); StepVerifier.create(employeeMono) .expectNextMatches(employee -> employee.getRole() .equals(Role.LEAD_ENGINEER)) .verifyComplete(); }
When the actual API call is made from the getEmployeeById(Integer employeeId) method in our EmployeeService class, MockWebServer will respond with the queued stub.
4.4. Checking a Request
We may also want to make sure that the MockWebServer was sent the correct HttpRequest.
MockWebServer has a handy method named takeRequest that returns an instance of RecordedRequest:
RecordedRequest recordedRequest = mockBackEnd.takeRequest(); assertEquals("GET", recordedRequest.getMethod()); assertEquals("/employee/100", recordedRequest.getPath());
With RecordedRequest, we can verify the HttpRequest that was received to make sure our WebClient sent it correctly.
5. Conclusion
In this tutorial, we tried the two main options available to mock WebClient based REST client code.
While Mockito worked and may be a good option for simple examples, the recommended approach is to use MockWebServer.
As always, the source code for this article is available over on GitHub.