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

如何在Java中对Spring WebClient进行单元测试?报错排查

问题描述

我有一段使用WebClient调用第三方API的代码:

public Mono<JsonNode> callApi(String url) {
    return webClient.get()
            .uri(url)
            .headers(httpHeaders -> httpHeaders.set(Constants.X_API_KEY, apiKey))
            .retrieve()
            .onStatus(HttpStatus::is5xxServerError,
                    res -> {
                        res.bodyToMono(String.class)
                                .subscribe(e -> log.error(Constants.EXCEPTION_LOG, e));
                        return Mono.error(new RetryableException("Server error: " + res.rawStatusCode()));
                    })
            .onStatus(HttpStatus::is4xxClientError,
                    res -> {
                        res.bodyToMono(String.class)
                                .subscribe(e -> log.error("Exception occurred in callPartnerApi: No retries {}", e));
                        return Mono.error(new Exception("Exception occurred calling partner api, no retries " + res.rawStatusCode()));
                    })
            .bodyToMono(JsonNode.class);
}

我尝试用Mockito编写单元测试,但测试失败:

@Test
void testCallPartnerApi_then5xxException() {
    WebClient.RequestHeadersUriSpec requestHeadersUriSpec = mock(WebClient.RequestHeadersUriSpec.class);
    WebClient.RequestHeadersSpec requestHeadersSpec = mock(WebClient.RequestHeadersSpec.class);
    WebClient.ResponseSpec responseSpec = mock(WebClient.ResponseSpec.class);

    when(webClient.get()).thenReturn(requestHeadersUriSpec);
    when(requestHeadersUriSpec.uri(anyString())).thenReturn(requestHeadersSpec);
    when(requestHeadersSpec.headers(any())).thenReturn(requestHeadersSpec);
    when(requestHeadersSpec.retrieve()).then(invocationOnMock -> Mono.error(new RetryableException("Server error: 500")));

    when(responseSpec.onStatus(argThat(x -> x.test(HttpStatus.INTERNAL_SERVER_ERROR)), any())).thenAnswer(invocation -> Mono.error(new RetryableException("Server error: 500")));


    StepVerifier.create(partnerService.callPartnerApi("/test"))
            .expectError()
            .verify();
}

得到的报错信息为:

java.lang.ClassCastException: class reactor.core.publisher.MonoError cannot be cast to class org.springframework.web.reactive.function.client.WebClient$ResponseSpec

请问必须使用WireMock或MockServerTest这类库才能测试这类场景吗?

问题原因与解决方案

报错根源

你的Mockito模拟逻辑存在类型错误:requestHeadersSpec.retrieve()方法的返回值类型是WebClient.ResponseSpec,但你让它返回了Mono.error实例。代码中调用retrieve()后会链式调用onStatus,而Mono对象无法被转换成ResponseSpec,自然触发ClassCastException。

无需依赖WireMock,修正Mockito模拟即可

不需要一定要用WireMock这类HTTP模拟库,只要正确Mock WebClient的链式调用逻辑就能完成测试,修正步骤如下:

  1. 保证链式调用的类型一致性:retrieve()必须返回你mock好的responseSpec实例,onStatus方法也需要返回responseSpec以维持链式调用,最后在bodyToMono中抛出目标异常。

  2. 修正后的测试代码:

@Test
void testCallPartnerApi_then5xxException() {
    WebClient.RequestHeadersUriSpec requestHeadersUriSpec = mock(WebClient.RequestHeadersUriSpec.class);
    WebClient.RequestHeadersSpec requestHeadersSpec = mock(WebClient.RequestHeadersSpec.class);
    WebClient.ResponseSpec responseSpec = mock(WebClient.ResponseSpec.class);

    // 模拟WebClient基础链式调用
    when(webClient.get()).thenReturn(requestHeadersUriSpec);
    when(requestHeadersUriSpec.uri(anyString())).thenReturn(requestHeadersSpec);
    when(requestHeadersSpec.headers(any())).thenReturn(requestHeadersSpec);
    // retrieve()返回mock的ResponseSpec,保证类型正确
    when(requestHeadersSpec.retrieve()).thenReturn(responseSpec);

    // 模拟5xx状态的onStatus处理:返回自身以支持后续链式调用
    when(responseSpec.onStatus(HttpStatus::is5xxServerError, any())).thenReturn(responseSpec);
    // 模拟bodyToMono抛出预期的重试异常
    when(responseSpec.bodyToMono(JsonNode.class)).thenReturn(Mono.error(new RetryableException("Server error: 500")));

    StepVerifier.create(partnerService.callApi("/test"))
            .expectError(RetryableException.class)
            .verify();
}

额外优化建议

原业务代码里res.bodyToMono(String.class).subscribe()的写法存在问题:在Reactor流中直接调用subscribe会脱离主数据流上下文,可能导致日志打印不及时或丢失。建议改为融入流的写法:

res.bodyToMono(String.class)
   .doOnNext(e -> log.error(Constants.EXCEPTION_LOG, e))
   .then(Mono.error(new RetryableException("Server error: " + res.rawStatusCode())))

何时需要用WireMock?

如果需要验证真实的HTTP交互细节(比如请求头、参数、响应体格式是否符合预期),或者业务代码包含复杂的HTTP逻辑(如重试、熔断、请求签名),WireMock这类工具会更合适,能模拟真实的第三方API行为。但单纯测试异常分支逻辑,用Mockito完全足够。

内容的提问来源于stack exchange,提问作者Gojo

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.05 01:20:46