英文:
How can I unit test an ExchangeFilterFunction?
问题
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?
Update
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 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?
Update
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?
答案1
得分: 0
你可以使用 @RunWith(MockitoJUnitRunner.Silent.class)
。 这个Mockito JUnit Runner实现忽略了存根参数不匹配 (MockitoJUnitRunner.StrictStubs) 并且不会检测到未使用的存根。
但这不是一个解决方案!
总是更好地修复你的测试。检测到不必要的存根 - 意味着你使用了一个不必要的when()存根,所以你需要移除它或者使用lenient()以停止异常。
Mockito.lenient().when(mockResponse.statusCode()).thenReturn(HttpStatusCode.valueOf(tooManyRequestsErrorCode));
顺便说一下,你的代码看起来有点奇怪。也许我们需要进行一些重构?在你的代码中,flatMap接受Mono和Publisher类型,所以我们可以轻松地返回模拟响应而不是Mono.just(mockResponse)。移除嵌套的monos:
@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()) {
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());
}
通过简化这段代码,一切都应该正常工作。
英文:
You can use @RunWith(MockitoJUnitRunner.Silent.class)
. This Mockito JUnit Runner implementation ignores stubbing argument mismatches (MockitoJUnitRunner.StrictStubs) and does not detect unused stubbings.
But this is not a solution!
Always better to fix your test. Unnecessary stubbings detected - means that you used an unnecessary when() stub so you need to remove it or use lenient() which stops the exception.
Mockito.lenient().when(mockResponse.statusCode()).thenReturn(HttpStatusCode.valueOf(tooManyRequestsErrorCode));
By the way, your code seems strange. Maybe we need to refactor some? In your code flatMap accepts both Mono and Publisher types so we can easily return mock response instead of Mono.just(mockResponse). Removing nesting monos:
@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()) {
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());
}
By simplifying this code, all should work.
答案2
得分: 0
以下是您要翻译的部分:
这是我的核心解决方案。它只能关注ExchangeFilterFunction,没有其他副作用。
英文:
It is my core solution. It can focus only to ExchangeFilterFunction without other side effects.
import java.net.URI;
import java.util.function.Function;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.http.client.reactive.ClientHttpRequest;
import org.springframework.http.client.reactive.ClientHttpResponse;
import org.springframework.mock.http.client.reactive.MockClientHttpRequest;
import org.springframework.mock.http.client.reactive.MockClientHttpResponse;
import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import org.springframework.web.reactive.function.client.ExchangeFunction;
import org.springframework.web.reactive.function.client.ExchangeFunctions;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
class ExchangeFilterFunctionTest {
@Test
void filterTest() {
ClientHttpResponse response = new MockClientHttpResponse(HttpStatus.BAD_REQUEST);
ClientRequest request = ClientRequest.create(HttpMethod.GET, URI.create("http://localhost/foo?bar=baz")).build();
ExchangeFunction next = ExchangeFunctions.create(new MyConnector(response));
Mono<ClientResponse> result = new Filter().filter(request, next);
StepVerifier.create(result)
.expectNextMatches(item -> item.rawStatusCode() == 400)
.verifyComplete();
}
class Filter implements ExchangeFilterFunction {
@Override
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
return next.exchange(request);
}
}
class MyConnector implements ClientHttpConnector {
private final ClientHttpResponse response;
MyConnector(ClientHttpResponse response) {
this.response = response;
}
@Override
public Mono<ClientHttpResponse> connect(HttpMethod method, URI uri, Function<? super ClientHttpRequest, Mono<Void>> requestCallback) {
return requestCallback.apply(new MockClientHttpRequest(method, uri))
.thenReturn(response);
}
}
}
答案3
得分: 0
我找出了问题所在。我没有像这样在单元测试中重新分配Mono:Mono<ClientResponse> clientResponseMono = retryStrategyFilter.filter(mockRequest, mockNext);
。
请查看下面的可工作代码。
@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();
}
英文:
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();
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论