You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 09:52:07