Spring WebFlux JUnit测试onErrorResume未覆盖Unauthorized异常分支
问题描述
现有基于Spring WebFlux实现、通过WebClient发起请求的服务方法,核心代码如下:
public Mono<String> fetch(String body, HttpHeaders headers, String correlationId) { Mono<String> response; try { response = postRequest(URI, headers, body); } catch (Exception exception) { throw new ExceptionHandler(HttpStatus.BAD_REQUEST, ErrorCode.REQUEST_ERROR); } return response; } private Mono<String> postRequest(String uri, HttpHeaders headers, String body) { return getStringMono(uri, headers, body).onErrorResume(exception -> { if (exception instanceof WebClientResponseException.Unauthorized) { accessTokenCache.invalidate(ACCESS_TOKEN); String updatedToken = accessTokenCache.getIfPresent(ACCESS_TOKEN); headers.replace(AUTHORIZATION, Collections.singletonList(updatedToken)); return getStringMono(uri, headers, body); } else { log.info(exception.getMessage()); throw new ExceptionHandler(HttpStatus.BAD_REQUEST, ErrorCode.REQUEST_ERROR); } }); } private Mono<String> getStringMono(String uri, HttpHeaders headers, String body) { return webClient.post().uri(uri).headers(httpHeaders -> httpHeaders.addAll(headers)) .body(Mono.just(body), String.class).retrieve().bodyToMono(String.class); }
编写JUnit单元测试尝试覆盖onErrorResume中401未授权的重试分支时,逻辑无法进入对应if判断,执行流直接进入fetch方法的外层catch块。原有测试代码如下:
@Test public void testUnauthorizedException() { RequestBodyUriSpec reqBodyUriMock = mock(WebClient.RequestBodyUriSpec.class); RequestBodySpec reqBodyMock = mock(WebClient.RequestBodySpec.class); RequestHeadersSpec reqHeaderMock = mock(WebClient.RequestHeadersSpec.class); ResponseSpec responseSpec = mock(WebClient.ResponseSpec.class); when(webClient.post()).thenReturn(reqBodyUriMock); when(reqBodyUriMock.uri(ArgumentMatchers.<String>notNull())).thenReturn(reqBodyMock); when(reqBodyMock.headers(any())).thenReturn(requestBodySpec); when(reqBodyMock.body(any(), eq(String.class))).thenReturn(reqHeaderMock); when(reqHeaderMock.retrieve()).thenReturn(responseSpec); ExchangeFunction exchangeFunction = mock(ExchangeFunction.class); given(exchangeFunction.exchange(any(ClientRequest.class))).willReturn(Mono.error(WebClientResponseException.Unauthorized.create(HttpStatus.UNAUTHORIZED.value(), "Unauthorized", null, null, null))); when(responseSpec.bodyToMono(ArgumentMatchers.<Class<String>>notNull())) .thenThrow(WebClientResponseException.Unauthorized.create(HttpStatus.UNAUTHORIZED.value(), "Unauthorized", null, null, null)); ExceptionHandler exception = assertThrows( ExceptionHandler.class, () -> client.fetch(body, headers, correlationId)); assertEquals(ErrorCode.REQUEST_ERROR, exception.getMessage()); }
问题根因
测试用例存在3个核心问题,导致逻辑无法命中目标分支:
- 异常抛出时机错误:WebClient的链式调用属于响应式流水线的同步组装阶段,只有对返回的
Mono触发订阅(subscribe/block/StepVerifier执行)时,才会真正发起请求、触发响应式错误信号。原有测试在bodyToMono的mock上使用thenThrow同步抛出异常,异常会在流水线组装阶段直接抛出,立刻被fetch方法外层的try-catch捕获,根本不会进入Mono的onErrorResume错误处理链路。 - Mock逻辑存在错误和缺失:
reqBodyMock.headers(any())的mock返回了未定义的requestBodySpec对象,链式调用会直接中断;多余的ExchangeFunctionmock没有注入到WebClient实例中,属于无效代码;同时没有模拟401触发后token刷新、第二次重试请求的返回逻辑。 - 未触发响应式订阅:测试直接同步调用
client.fetch(),此时仅完成Mono的组装,没有触发订阅,整个请求、错误处理、重试逻辑完全不会执行。
修复后的测试用例
调整异常mock方式,使用Mono.error()包装401异常,让错误在订阅阶段以响应式信号的形式抛出;补全链式调用mock逻辑,增加token刷新、重试场景的模拟;使用StepVerifier触发Mono订阅,保证响应式逻辑完整执行。
@Test public void testUnauthorizedException() { // Mock WebClient链式调用节点 RequestBodyUriSpec reqBodyUriMock = mock(WebClient.RequestBodyUriSpec.class); RequestBodySpec reqBodyMock = mock(WebClient.RequestBodySpec.class); RequestHeadersSpec reqHeaderMock = mock(WebClient.RequestHeadersSpec.class); ResponseSpec responseSpec = mock(WebClient.ResponseSpec.class); when(webClient.post()).thenReturn(reqBodyUriMock); when(reqBodyUriMock.uri(ArgumentMatchers.<String>notNull())).thenReturn(reqBodyMock); // 修复链式调用返回值笔误 when(reqBodyMock.headers(any())).thenReturn(reqBodyMock); when(reqBodyMock.body(any(), eq(String.class))).thenReturn(reqHeaderMock); when(reqHeaderMock.retrieve()).thenReturn(responseSpec); // 构造401响应式异常,禁止使用thenThrow同步抛出 WebClientResponseException unauthorizedEx = WebClientResponseException.Unauthorized .create(HttpStatus.UNAUTHORIZED.value(), "Unauthorized", null, null, null); // 模拟第一次请求返回401,刷新token后第二次请求返回成功 when(responseSpec.bodyToMono(ArgumentMatchers.<Class<String>>notNull())) .thenReturn(Mono.error(unauthorizedEx)) .thenReturn(Mono.just("mock success response")); // 初始化token缓存,模拟token刷新逻辑 String oldToken = "expired-token"; String newToken = "valid-token"; accessTokenCache.put(ACCESS_TOKEN, oldToken); doAnswer(invocation -> { accessTokenCache.put(ACCESS_TOKEN, newToken); return null; }).when(accessTokenCache).invalidate(ACCESS_TOKEN); // 使用StepVerifier触发Mono订阅,执行完整响应式链路 StepVerifier.create(client.fetch(body, headers, correlationId)) .expectNext("mock success response") .verifyComplete(); // 验证401分支逻辑确实执行:缓存被失效、请求头token被更新 verify(accessTokenCache, times(1)).invalidate(ACCESS_TOKEN); assertEquals(Collections.singletonList(newToken), headers.get(AUTHORIZATION)); }
如果需要测试401重试后依然失败的场景,只需要把第二次
bodyToMono的返回值调整为对应错误的Mono.error()即可,断言逻辑对应调整为预期异常。
内容的提问来源于stack exchange,提问作者Digital
相关产品推荐
相关产品推荐

