We wrote a small Spring Boot REST application, which performs a REST request on another REST endpoint.
@RequestMapping("/api/v1") @SpringBootApplication @RestController @Slf4j public class Application { @Autowired private WebClient webClient; @RequestMapping(value = "/zyx", method = POST) @ResponseBody XyzApiResponse zyx(@RequestBody XyzApiRequest request, @RequestHeader HttpHeaders headers) { webClient.post() .uri("/api/v1/someapi") .accept(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON) .body(BodyInserters.fromObject(request.getData())) .exchange() .subscribeOn(Schedulers.elastic()) .flatMap(response -> response.bodyToMono(XyzServiceResponse.class).map(r -> { if (r != null) { r.setStatus(response.statusCode().value()); } if (!response.statusCode().is2xxSuccessful()) { throw new ProcessResponseException( "Bad status response code " + response.statusCode() + "!"); } return r; })) .subscribe(body -> { // Do various things }, throwable -> { // This section handles request errors }); return XyzApiResponse.OK; } }
We are new to Spring and are having trouble writing a Unit Test for this small code snippet.
Is there an elegant (reactive) way to mock the webClient itself or to start a mock server that the webClient can use as an endpoint?
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)
The WebClient is part of spring-webflux module and we will add it as required dependency for Spring Reactive support. Keep in mind that Spring WebClient will be used on top of an existing asynchronous HTTP client library. We have the following options: Reactor Netty.
We accomplished this by providing a custom ExchangeFunction
that simply returns the response we want to the WebClientBuilder
:
webClient = WebClient.builder() .exchangeFunction(clientRequest -> Mono.just(ClientResponse.create(HttpStatus.OK) .header("content-type", "application/json") .body("{ \"key\" : \"value\"}") .build()) ).build(); myHttpService = new MyHttpService(webClient); Map<String, String> result = myHttpService.callService().block(); // Do assertions here
If we want to use Mokcito to verify if the call was made or reuse the WebClient accross multiple unit tests in the class, we could also mock the exchange function:
@Mock private ExchangeFunction exchangeFunction; @BeforeEach void init() { WebClient webClient = WebClient.builder() .exchangeFunction(exchangeFunction) .build(); myHttpService = new MyHttpService(webClient); } @Test void callService() { when(exchangeFunction.exchange(any(ClientRequest.class))) .thenReturn(buildMockResponse()); Map<String, String> result = myHttpService.callService().block(); verify(exchangeFunction).exchange(any()); // Do assertions here }
Note: If you get null pointer exceptions related to publishers on the when
call, your IDE might have imported Mono.when
instead of Mockito.when
.
Sources:
With the following method it was possible to mock the WebClient with Mockito for calls like this:
webClient .get() .uri(url) .header(headerName, headerValue) .retrieve() .bodyToMono(String.class);
or
webClient .get() .uri(url) .headers(hs -> hs.addAll(headers)); .retrieve() .bodyToMono(String.class);
Mock method:
private static WebClient getWebClientMock(final String resp) { final var mock = Mockito.mock(WebClient.class); final var uriSpecMock = Mockito.mock(WebClient.RequestHeadersUriSpec.class); final var headersSpecMock = Mockito.mock(WebClient.RequestHeadersSpec.class); final var responseSpecMock = Mockito.mock(WebClient.ResponseSpec.class); when(mock.get()).thenReturn(uriSpecMock); when(uriSpecMock.uri(ArgumentMatchers.<String>notNull())).thenReturn(headersSpecMock); when(headersSpecMock.header(notNull(), notNull())).thenReturn(headersSpecMock); when(headersSpecMock.headers(notNull())).thenReturn(headersSpecMock); when(headersSpecMock.retrieve()).thenReturn(responseSpecMock); when(responseSpecMock.bodyToMono(ArgumentMatchers.<Class<String>>notNull())) .thenReturn(Mono.just(resp)); return mock; }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With