Spring Top

The early-bird price of the new Learn Spring Security OAuth course packages will increase by $50 on Wednesday:

>> CHECK OUT THE COURSE

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:

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 webserver:

@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 maybe 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.

Spring bottom

The early-bird price of the new Learn Spring Security OAuth course packages will increase by $50 on Wednesday:

>> CHECK OUT THE COURSE
Comments are closed on this article!