I am trying to create a global webclient retry filter and trying to unit test it via Mockito. The filter is an implementation of an ExchangeFilterFunction. I am using the technique proposed here.
I am using Spring Boot version 3.0.6, and java temurin 17.
RetryStrategyFilter
@Override
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
return next.exchange(request)
.flatMap(clientResponse -> Mono.just(clientResponse)
.filter(response -> {
final HttpStatusCode code = clientResponse.statusCode();
boolean isRetryable = Boolean.FALSE;
if (code.isError()) {
//check if its a retryable error
isRetryable = Arrays.stream(DEFAULT_RETRYABLE_ERROR_CODES).anyMatch(defaultCode -> defaultCode == code.value()) ||
(retryFilterConfiguration.getRetryErrorCodes() != null &&
retryFilterConfiguration.getRetryErrorCodes().stream().anyMatch(retryErrorCode -> retryErrorCode == code.value()));
LOGGER.warn("Request Failed. Retrying -> url={}; status={}", request.url(), code.value());
}
return isRetryable;
}) // if no errors, filter it out
.flatMap(response -> clientResponse.createException()) // let's raise an exception if response was an error
.flatMap(Mono::error) // trigger onError signal
.thenReturn(clientResponse)
)
.retry(retryFilterConfiguration.getRetryCount());
}
RetryStrategyFilterTest
@DisplayName("Retry on Default Error Code")
@Test
public void defaultRetryableErrorCode(){
//ARRANGE
ClientRequest mockRequest = Mockito.mock(ClientRequest.class);
ExchangeFunction mockNext = Mockito.mock(ExchangeFunction.class);
ClientResponse mockResponse = Mockito.mock(ClientResponse.class, RETURNS_DEEP_STUBS);
int tooManyRequestsErrorCode = HttpStatus.TOO_MANY_REQUESTS.value();
when(mockNext.exchange(mockRequest)).thenReturn(Mono.just(mockResponse));
when(mockResponse.statusCode()).thenReturn(HttpStatusCode.valueOf(tooManyRequestsErrorCode));
//ACT
retryStrategyFilter.filter(mockRequest,mockNext);
//ASSERT
verify(mockResponse,times(0)).createException();
verify(retryFilterConfiguration,times(1)).getRetryCount();
}
When unit testing the code with a default retriable error code (429), I am expecting it to call retry, but I am getting the following stub error.
Unnecessary stubbings detected.
Clean & maintainable test code requires zero unnecessary code.
Following stubbings are unnecessary (click to navigate to relevant line of code):
1. -> at org.mycomp.http.filter.RetryStrategyFilterTest.defaultRetryableErrorCode(RetryStrategyFilterTest.java:37)
Please remove unnecessary stubbings or use 'lenient' strictness. More info: javadoc for UnnecessaryStubbingException class.
org.mockito.exceptions.misusing.UnnecessaryStubbingException:
This would mean that the filter
function of the Mono.just(clientResponse)
isn't being called, and when I debug it, I notice that I am not able to step through it. One lead that I'm following is that I have this line in my test, when(mockNext.exchange(mockRequest)).thenReturn(Mono.just(mockResponse));
, and this line in the filter function .flatMap(clientResponse -> Mono.just(clientResponse)
. Am I nesting Mono's here? Could that be the reason that it is not working?
I've re-factored the filter function code to the following
@Override
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
return next.exchange(request)
.flatMap(clientResponse -> {
final HttpStatusCode code = clientResponse.statusCode();
boolean isRetryable = Boolean.FALSE;
if (code.isError()) {
//check if its a retryable error
isRetryable = Arrays.stream(DEFAULT_RETRYABLE_ERROR_CODES).anyMatch(defaultCode -> defaultCode == code.value()) ||
(retryFilterConfiguration.getRetryErrorCodes() != null &&
retryFilterConfiguration.getRetryErrorCodes().stream().anyMatch(retryErrorCode -> retryErrorCode == code.value()));
LOGGER.warn("Request Failed. Retrying -> url={}; status={}", request.url(), code.value());
}
if (isRetryable){
return clientResponse.createException()
.flatMap(Mono::error);
}else{
return Mono.just(clientResponse);
}
}
).retry(retryFilterConfiguration.getRetryCount());
}
I've also updated the verification on my test to the following which includes the StepVerifier
since the flatmap
is async.
@DisplayName("Retry on Default Error Code")
@Test
public void defaultRetryableErrorCode(){
//ARRANGE
ClientRequest mockRequest = Mockito.mock(ClientRequest.class);
ExchangeFunction mockNext = Mockito.mock(ExchangeFunction.class);
ClientResponse mockResponse = Mockito.mock(ClientResponse.class);
Mono<ClientResponse> monoResponse = Mono.just(mockResponse);
int tooManyRequestsErrorCode = HttpStatus.TOO_MANY_REQUESTS.value();
when(mockNext.exchange(mockRequest)).thenReturn(monoResponse);
when(mockResponse.statusCode()).thenReturn(HttpStatusCode.valueOf(tooManyRequestsErrorCode));
//ACT
retryStrategyFilter.filter(mockRequest,mockNext);
//ASSERT
StepVerifier.create(monoResponse)
.verifyError();
}
I am getting this error.
expectation "expectError()" failed (expected: onError(); actual: onNext(Mock for ClientResponse, hashCode: 836386144))
java.lang.AssertionError: expectation "expectError()" failed (expected: onError(); actual: onNext(Mock for ClientResponse, hashCode: 836386144))
at reactor.test.MessageFormatter.assertionError(MessageFormatter.java:115)
When I debug, I still notice that it still isn't going into the flatmap
lambda function for the retry filter. Anyone have any idea?
I figured out the issue. I wasn't assigning the mono back in the unit test like this Mono<ClientResponse> clientResponseMono = retryStrategyFilter.filter(mockRequest,mockNext);
.
Please see the working code below.
@DisplayName("Retry on Default Error Code")
@Test
public void defaultRetryableErrorCode(){
//ARRANGE
ClientRequest mockRequest = Mockito.mock(ClientRequest.class);
ExchangeFunction mockNext = Mockito.mock(ExchangeFunction.class);
ClientResponse mockResponse = Mockito.mock(ClientResponse.class);
int tooManyRequestsErrorCode = HttpStatus.TOO_MANY_REQUESTS.value();
when(mockNext.exchange(mockRequest)).thenReturn(Mono.just(mockResponse));
when(mockResponse.statusCode()).thenReturn(HttpStatusCode.valueOf(tooManyRequestsErrorCode));
//ACT
Mono<ClientResponse> clientResponseMono = retryStrategyFilter.filter(mockRequest,mockNext);
//ASSERT
StepVerifier.create(clientResponseMono)
.verifyError();
}