Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to unit test the Spring WebClient in java?

I have this code which uses WebClient to call a third party API.

    public Mono<JsonNode> callApi(String url) {
        return webClient.get()
                .uri(url)
                .headers(httpHeaders -> httpHeaders.set(Constants.X_API_KEY, apiKey))
                .retrieve()
                .onStatus(HttpStatus::is5xxServerError,
                        res -> {
                            res.bodyToMono(String.class)
                                    .subscribe(e -> log.error(Constants.EXCEPTION_LOG, e));
                            return Mono.error(new RetryableException("Server error: " + res.rawStatusCode()));
                        })
                .onStatus(HttpStatus::is4xxClientError,
                        res -> {
                            res.bodyToMono(String.class)
                                    .subscribe(e -> log.error("Exception occurred in callPartnerApi: No retries {}", e));
                            return Mono.error(new Exception("Exception occurred calling partner api, no retries " + res.rawStatusCode()));
                        })
                .bodyToMono(JsonNode.class);
    }

I am trying to use Mockito to unit test this and so far I have this which is failing:

    @Test
    void testCallPartnerApi_then5xxException() {
        WebClient.RequestHeadersUriSpec requestHeadersUriSpec = mock(WebClient.RequestHeadersUriSpec.class);
        WebClient.RequestHeadersSpec requestHeadersSpec = mock(WebClient.RequestHeadersSpec.class);
        WebClient.ResponseSpec responseSpec = mock(WebClient.ResponseSpec.class);

        when(webClient.get()).thenReturn(requestHeadersUriSpec);
        when(requestHeadersUriSpec.uri(anyString())).thenReturn(requestHeadersSpec);
        when(requestHeadersSpec.headers(any())).thenReturn(requestHeadersSpec);
        when(requestHeadersSpec.retrieve()).then(invocationOnMock -> Mono.error(new RetryableException("Server error: 500")));

        when(responseSpec.onStatus(argThat(x -> x.test(HttpStatus.INTERNAL_SERVER_ERROR)), any())).thenAnswer(invocation -> Mono.error(new RetryableException("Server error: 500")));


        StepVerifier.create(partnerService.callPartnerApi("/test"))
                .expectError()
                .verify();
    }

The error I get from this test is this:

java.lang.ClassCastException: class reactor.core.publisher.MonoError cannot be cast to class org.springframework.web.reactive.function.client.WebClient$ResponseSpec

Is using a library like WireMock or MockServerTest the only way to test these scenarios?

like image 383
Gojo Avatar asked Nov 02 '25 03:11

Gojo


2 Answers

Unit testing WebClient has very low ROI. I would recommend looking at WireMock that provides very good API for testing web clients. Here are some examples

stubFor(get(url)
        .withHeader(HttpHeaders.ACCEPT, containing(MediaType.APPLICATION_JSON_VALUE))
        .willReturn(aResponse()
                .withStatus(200)
                .withBody("{}")
        )
);

StepVerifier.create(service.callApi())
        .expectNextCount(1)
        .verifyComplete();

You could easily test both positive and negative scenarios by providing different stubs

stubFor(get(url)
        .withHeader(HttpHeaders.ACCEPT, containing(MediaType.APPLICATION_JSON_VALUE))
        .willReturn(aResponse()
                .withStatus(500)
        )
);

In addition, you could test retry logic using Scenarios or even simulate timeouts using Delays.

To initialize it you could use @AutoConfigureWireMockprovided by org.springframework.cloud:spring-cloud-contract-wiremock or, as an alternative, you can add direct dependency to WireMock and initialize it explicitly.

like image 81
Alex Avatar answered Nov 03 '25 19:11

Alex


We use Wiremock to test Client functionality. It can be done in standard unit test or a SpringBoot test.

Below code explains the Unit testing in regular tests.

@Service
class MoviesClient {
  private final WebClient moviesWebClient;

  public Mono<Movie> findMovieById(String movieID) {
    return moviesWebClient.get().uri(MOVIE_ENDPOINT+"/"+movieID).bodyValue(job).retrieve()
        .bodyToMono(Movie.class);
  }
}

Now as we see MoviesClient needs webClient to call the movie service and get movie.

  @ExtendWith({MockitoExtension.class})
    class MoviesClientTest {
    
        private MoviesClient moviesClient;
        
        @RegisterExtension
          private static final WireMockExtension MOVIES_WM =
         WireMockExtension.newInstance().options(wireMockConfig().port("8086")).build();
        
        @BeforeEach
        void setup() {
        
        var webClient = WebClient.builder().baseUrl("http://localhost:8086")
                .defaultHeader(CONTENT_TYPE, APPLICATION_JSON_VALUE).build();
        moviesClient = new MoviesClient(moviesClient)
MOVIES_WM.stubFor(get(MOVIE_ENDPOINT+"/"+"1234")).withHeader(CONTENT_TYPE, matching(APPLICATION_JSON_VALUE))willReturn(aResponse().withStatus(OK.value()).withHeader(CONTENT_TYPE, APPLICATION_JSON_VALUE)
            .withBody("Your json response body comes here")));
        }
}

Now you can write your tests to call the method on client which in turn will make a webclient call which will be stubbed to wiremock server.

@Test
void testMoviesEndPoint() {
  StepVerifier.create(moviesClient.findMovieById(1234)
  .expectNext(movie -> assertThat(movie).getId.equals(1234))
  .verifyComplete();

FLUENT_WM.verify(1, postRequestedFor(urlEqualTo(MOVIE_ENDPOINT+"/"+"1234")));
}
like image 28
Sanjay Bharwani Avatar answered Nov 03 '25 19:11

Sanjay Bharwani



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!