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?
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.
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")));
}
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