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

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对象,链式调用会直接中断;多余的ExchangeFunction mock没有注入到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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.26 17:48:21