如何对ExchangeFilterFunction实现的重试过滤器进行单元测试?
问题:WebClient全局重试过滤器单元测试异常排查
基于Spring Boot 3.0.6和Java Temurin 17实现WebClient全局重试过滤器(ExchangeFilterFunction实现类)时,单元测试阶段遇到两类问题:
- 初始版本测试触发
UnnecessaryStubbingException,提示存在不必要的存根,调试发现Mono.just(clientResponse)的filter方法未执行; - 重构过滤器代码后改用
StepVerifier做异步验证,出现断言错误:预期onError信号,实际收到onNext,且flatMap逻辑未执行。
初始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()); }
初始测试代码
@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(); }
初始错误信息
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:
重构后的RetryStrategyFilter代码
@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()); }
重构后的测试代码
@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(); }
重构后的错误信息
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)
问题解决思路
针对初始版本的UnnecessaryStubbingException
- Mono未订阅导致逻辑未执行:Reactor异步逻辑只有在订阅后才会触发执行,初始测试中调用
filter()后未订阅返回的Mono,导致mockResponse.statusCode()的存根从未被调用,Mockito严格模式判定为不必要存根。 - 验证逻辑错误:初始代码中如果是可重试错误,
filter会保留响应并执行flatMap(clientResponse.createException()),因此verify(mockResponse,times(0)).createException()的断言逻辑与实际代码行为矛盾。
针对重构版本的断言错误
- 验证对象错误:测试中
StepVerifier验证的是原始的monoResponse,而非过滤器处理后返回的新Mono。filter()方法会生成经过flatMap和retry处理的新数据流,必须对这个返回值进行验证。 - 存根缺失:重构后的过滤器在可重试分支会调用
clientResponse.createException(),但测试中未对该方法存根,导致执行时可能抛出未预期异常或分支走偏。 - 重试配置未生效:需确保
retryFilterConfiguration.getRetryCount()返回大于0的数值,否则retry操作不会触发;同时确认DEFAULT_RETRYABLE_ERROR_CODES包含429状态码。
通用修复步骤
- 触发Mono订阅:所有Reactor异步逻辑必须通过
StepVerifier或block()触发订阅,才能执行内部逻辑。 - 验证正确的数据流:
StepVerifier.create()的入参必须是filter()方法返回的Mono,而非原始响应Mono。 - 补充必要存根:对
clientResponse.createException()进行存根,例如when(mockResponse.createException()).thenReturn(Mono.error(new RuntimeException()))。 - 检查配置有效性:确认重试次数配置大于0,默认错误码列表包含目标状态码。
- 调整Mockito严格模式:若需保留暂时未调用的存根,可使用
lenient()标记,例如lenient().when(mockResponse.statusCode()).thenReturn(...)。
内容的提问来源于stack exchange,提问作者Juan Bustamante
相关产品推荐
相关产品推荐

