Testing Spring reactive WebClient

Spring WebClient is the reactive replacement for the legacy RestTemplate. It has a more modern fluent API and first class support for Reactive Streams. This means it supports non-blocking, asynchronous responses.

However, the reactive WebClient does not yet have the mature test support that RestTemplate now has. There is not yet a standard recipe to test Spring WebClient applications. No doubt support will be improved in future versions but for now, here’s what works for me.

Unit or integration test?

Spring documentation is currently short on detail on how to test WebClient implementations. Two broad solutions to testing are:

  1. Start up a (mock) web server, run tests against it and make assertions on what it receives or;
  2. Mock the WebClient

For the first option, Baeldung suggests using Square developer team’s MockWebServer. This is a web server that allows you to control responses and verify incoming requests. It’s powerful but fiddly to set up. You’ll need to set up the mock web server before and stop it after every test. I prefer the second option.

Mocking WebClient

WebClient can be fiddly to mock due to its fluent API. A typical call to this API looks like what Uncle Bob would call a train wreck:

RuokResponse response = webClient
        .get()
        .uri(uri)
        .retrieve()
        .bodyToMono(RuokResponse.class)
        .block();

If we want to mock / stub the WebClient behaviour, do we also need to stub behaviour of the return types of get(), uri(), retrieve(), and bodyToMono()?

It turns out we don’t. The heart of the WebClient implementation is the ExchangeFunction. If we mock that, we have complete control over request / response behaviour of the WebClient and we can use the real implementation of everything else.

Test setup requires creation of a mock ExchangeFunction, then inject that into the (real) WebClient, then inject the WebClient into the class under test:

@ExtendWith(MockitoExtension.class)
class ZooKeeperCommandClientTest {

    private final ExchangeFunction exchangeFunction = mock(ExchangeFunction.class);
    private final WebClient webClient = WebClient.builder().exchangeFunction(exchangeFunction).build();
    private final ZooKeeperCommandClient client = new ZooKeeperCommandClient(webClient);

You can then stub the response from the (mock) webserver like this:

ClientResponse response = ClientResponse.create(HttpStatus.OK)
        .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
        .body("{ \"command\": \"ruok\" }").build();
when(exchangeFunction.exchange(any(ClientRequest.class))).thenReturn(Mono.just(response));

You can also stub error responses:

ConnectException connEx = new ConnectException("Simulated connection exception");
when(exchangeFunction.exchange(any(ClientRequest.class))).thenThrow(new WebClientRequestException(connEx, HttpMethod.GET, new URI("http://peer.host:8080/commands/ruok"), new HttpHeaders()));

Given this stubbed behaviour, you can very easily make assertions on your code’s handling of responses:

@Test
void okResponse_returnsOkStatus() {
    stubOkResponse();
    Peer.Status status = client.ruok(PEER);
    assertEquals(Peer.Status.OK, status);
}

You can also make assertions on the mocked WebClient by capturing requests in the mock ExchangeFunction:

    @Test
    void ruok_sendsGetRequest() {
        stubOkResponse();

        client.ruok(PEER);

        verify(exchangeFunction).exchange(requestCaptor.capture());
        ClientRequest request = requestCaptor.getValue();
        assertEquals(HttpMethod.GET, request.method());
        assertEquals("http://peer.host:8080/commands/ruok", request.url().toString());
    }

To see a complete working example, take a look at my tests for a Zookeeper command client in the Zookidash project.

Leave a Reply

Your email address will not be published.