WebClient单元测试中doOnError/onError方法未触发求助
我有一个WebClient异步方法getAsyncCollection,生产环境运行正常,但单元测试失败。测试模拟ClientHttp4xxClient异常时,isServerError()和throwHttpClientException()都没被调用,断言抛出指定异常的逻辑失败。调试发现URI桩和参数都正常,但错误处理方法未执行,测试因断言无异常抛出而失败。
业务方法(生产正常,测试失败)
@Override public <T> Flux<T> getAsyncCollection(ParameterizedTypeReference<T> type, String baseUrl, String endpointPath, Map<String, String> pathVariables, Map<String, String> queryParameters , int maxNumberOfRetries, long delay) { Flux<T> collectionResponse = null; try { WebClient webClient = webClientConfig.getWebClient(); collectionResponse = webClient .get() .uri(getURI(baseUrl, endpointPath, pathVariables, queryParameters)) .retrieve() .bodyToFlux(type) .retryWhen(Retry.fixedDelay(maxNumberOfRetries, Duration.ofMillis(delay)). filter(this::isServerError) .onRetryExhaustedThrow((retryBackoffSpec, retrySignal) -> retrySignal.failure())) .doOnError(ClientHttp4xxClient.class, this::throwHttpClientException); } catch (HttpClientException httpClientException) { throw httpClientException; } catch (Exception exception) { throw new HttpServerException(exception.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR, exception); } return collectionResponse; } private boolean isServerError(Throwable throwable) { return !(ClientHttp4xxClient.class.isAssignableFrom(throwable.getClass())); } private void throwHttpClientException(ClientHttp4xxClient clientHttp4xxClientException) { // ... 具体实现 }
测试代码
@TestInstance(TestInstance.Lifecycle.PER_CLASS) @ExtendWith(MockitoExtension.class) class HttpWebClientTest { @Mock private WebClientConfig webClientConfig; private WebClient webClient; private HttpRestClient httpRestClient; WebClient.RequestHeadersUriSpec requestHeadersUriSpec; WebClient.RequestHeadersSpec requestHeadersSpec; WebClient.ResponseSpec responseSpec; @BeforeEach void setUp() { webClient = mock(WebClient.class); when(webClientConfig.getWebClient()).thenReturn(webClient); httpRestClient = new HttpWebClient(webClientConfig); requestHeadersUriSpec = mock(WebClient.RequestHeadersUriSpec.class); requestHeadersSpec = mock(WebClient.RequestHeadersSpec.class); responseSpec = mock(WebClient.ResponseSpec.class); when(webClient.get()).thenReturn(requestHeadersUriSpec); } @ParameterizedTest @MethodSource("getClientHttp4xxClientExceptions") void asyncMethods_HttpClientException(ClientHttp4xxClient clientHttp4xxClient, Class expectedException) { setUpMocksForUriSpec(); when(responseSpec.bodyToFlux(new ParameterizedTypeReference<>())).thenReturn(Flux.error(clientHttp4xxClient)); Assertions.assertThrows(expectedException, () -> httpRestClient.getAsyncCollection(new ParameterizedTypeReference<>() {}, CustomerAgreementsMockData.CORE_CUAGR_BASE_URL, APIEndpoints.CORE_CUAGR_GET_CUSTOMER_AGREEMENTS, PATH_VARIABLES, QUERY_PARAMETERS, NO_OF_RETRIES, DELAY_IN_MILLISECONDS)); } private void setUpMocksForUriSpec() { when(requestHeadersUriSpec.uri(getURI(CustomerAgreementsMockData.CORE_CUAGR_BASE_URL, APIEndpoints.CORE_CUAGR_GET_CUSTOMER_AGREEMENTS, PATH_VARIABLES, QUERY_PARAMETERS))) .thenReturn(requestHeadersSpec); when(requestHeadersSpec.retrieve()).thenReturn(responseSpec); } }
1. Flux是惰性执行的,测试未触发订阅
WebFlux的Flux是惰性流,只有被订阅后才会触发整个请求链路和错误处理逻辑。生产环境中上游代码肯定有订阅操作(比如subscribe()、block()或者整合到Spring WebFlux的响应式接口中),但你的测试只调用了getAsyncCollection方法,没有订阅返回的Flux,导致错误处理代码根本没执行,自然不会抛出异常。
解决方法:
在测试中触发Flux执行,比如用blockFirst()或blockLast():
Assertions.assertThrows(expectedException, () -> httpRestClient.getAsyncCollection(...) .blockFirst()); // 根据业务场景选择blockFirst/blockLast
2. Mockito对ParameterizedTypeReference的匹配失效
测试中when(responseSpec.bodyToFlux(new ParameterizedTypeReference<>())).thenReturn(...)用了新创建的匿名实例,Mockito默认按对象相等性匹配,而不同的匿名类实例不相等,导致这个mock规则不会生效。实际调用时bodyToFlux传入的是另一个ParameterizedTypeReference实例,mock没匹配上,可能返回默认空Flux而非错误Flux。
解决方法:
用any()匹配任意ParameterizedTypeReference实例:
when(responseSpec.bodyToFlux(any(ParameterizedTypeReference.class))) .thenReturn(Flux.error(clientHttp4xxClient));
如果需要精确匹配类型,可以结合eq和类型判断(根据实际业务调整):
when(responseSpec.bodyToFlux(eq(new ParameterizedTypeReference<YourTargetType>() {}))) .thenReturn(Flux.error(clientHttp4xxClient));
3. 错误处理逻辑的执行验证
doOnError是副作用操作,不会改变Flux的错误流,throwHttpClientException如果是抛出异常,需要确保异常能被传递到订阅端。另外isServerError会过滤掉ClientHttp4xxClient异常,所以retryWhen不会重试这类异常,直接走到doOnError逻辑,但前提是Flux被订阅。
总结
优先解决Flux未订阅的问题,这是核心原因。再修复Mockito的参数匹配问题,确保mock的错误Flux能被正确返回。调整后错误处理逻辑会被触发,断言就能正常捕获异常。
内容的提问来源于stack exchange,提问作者Kaliyug Antagonist

