如何在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的链式调用逻辑就能完成测试,修正步骤如下:
保证链式调用的类型一致性:
retrieve()必须返回你mock好的responseSpec实例,onStatus方法也需要返回responseSpec以维持链式调用,最后在bodyToMono中抛出目标异常。修正后的测试代码:
@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

