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

WebTestClient链式Mono.onErrorResume行为异常,单元测试与Postman结果不符

WebFlux应用单元测试异常匹配问题及代码优化建议

问题背景

因工作需求接触响应式编程,正在开发首个Webflux应用实践。采用函数式端点实现,在TaskHandler中定义了createTask方法:

public Mono<ServerResponse> createTask(ServerRequest request) {
    Mono<TaskDto> taskToCreate = request
            .bodyToMono(TaskDto.class)
            .flatMap(body -> HandlerUtils.validateAndReturn(body, validator))
            .onErrorResume(ex -> {
                log.error("Error on createTask method: ", ex);
                if(ex instanceof TaskMgmtException) return Mono.error(ex);
                return Mono.error(new TaskMgmtException(TaskError.TE_004));
            });

    return taskService.create(taskToCreate)
            .onErrorResume(ex -> {
                log.error("Error at createTask method: ", ex);
                if(ex instanceof TaskMgmtException) return Mono.error(ex);
                return Mono.error(new TaskMgmtException(TaskError.TE_001));
            })
            .flatMap(taskDto -> {
                CommonResponse<TaskDto> commonResponse = new CommonResponse<>();
                commonResponse.setSuccess(true);
                commonResponse.setTimeStamp(LocalDateTime.now().toString());
                commonResponse.setData(taskDto);

                return ServerResponse
                        .status(HttpStatus.CREATED)
                        .contentType(MediaType.APPLICATION_JSON)
                        .bodyValue(commonResponse);
            });
}

同时在TaskService中实现了create方法:

@Override
public Mono<TaskDto> create(Mono<TaskDto> taskDto) {
    return taskDto
            .onErrorResume(ex -> {
                log.error("Error on create method", ex);
                if(ex instanceof TaskMgmtException) return Mono.error(ex);
                return Mono.error(new TaskMgmtException(TaskError.TE_005));
            })
            .flatMap(task -> {
                Task taskToAdd = new Task();

                taskToAdd.setTitle(task.getTitle());
                taskToAdd.setDescription(task.getDescription());
                taskToAdd.setDueDate(task.getDueDate());
                taskToAdd.setStatus(task.getTaskStatus());

                return taskRepository.save(taskToAdd).map(TaskMapper.TASK_MAPPER::toDto);
            });
}

通过.onErrorResume细分错误类型:服务层抛出TE_005表示数据库错误,处理器层验证失败抛出TE_004表示输入无效。

单元测试问题

编写的单元测试用例中,testCreateTaskFail期望返回Bad Request状态,但实际返回Internal Server Error,但Postman测试时行为正常。测试代码如下:

@ContextConfiguration(classes = {RouterConfig.class, TaskHandler.class, GlobalExceptionHandler.class})
@WebFluxTest(TaskHandler.class)
public class TaskHandlerUnitTest {

    @Autowired
    private ApplicationContext context;

    @MockBean
    TaskService taskService;

    @Autowired
    private WebTestClient webTestClient;

    private final List<TaskDto> taskList = new ArrayList<>();

    @BeforeEach
    void setup() {
        webTestClient = WebTestClient.bindToApplicationContext(context).build();
        // 初始化测试数据代码
    }

    
    @Test
    void testCreateTaskFail() {
        TaskDto incompleteTaskDto = new TaskDto();
        Mockito.when(taskService.create(any(Mono.class))).thenReturn(Mono.error(
                new TaskMgmtException(TaskError.TE_005)
        ));

        webTestClient.post()
                .uri("/tasks")
                .header("Content-Type", "application/json")
                .bodyValue(incompleteTaskDto)
                .exchange()
                .expectStatus().isBadRequest()
                .expectBody()
                .jsonPath("$.success").isEqualTo(false)
                .jsonPath("$.data").isEmpty();

        verify(taskService, times(1)).create(any(Mono.class));
    }
    // 其他测试用例
}

问题分析与解决方案

1. 单元测试配置问题

核心问题1:测试期望与错误码映射不匹配

从代码逻辑看,TE_005对应数据库错误,正常应映射为500 Internal Server Error,但测试用例错误地期望400 Bad Request,这是断言逻辑的错误。Postman测试正常是因为实际返回的就是500状态,与错误码定义一致。

核心问题2:手动重构WebTestClient破坏自动配置

@WebFluxTest已经自动配置并注入了包含全局异常处理器的WebTestClient,你在setup中手动调用bindToApplicationContext重构客户端,可能导致全局异常处理器未被正确加载。直接使用@Autowired注入的客户端即可,删除setup中的重构代码。

修改后的测试用例关键部分:

// 移除setup里的webTestClient重构代码
@BeforeEach
void setup() {
    // 只保留初始化测试数据的代码
}

@Test
void testCreateTaskFail() {
    TaskDto incompleteTaskDto = new TaskDto();
    Mockito.when(taskService.create(any(Mono.class))).thenReturn(Mono.error(
            new TaskMgmtException(TaskError.TE_005)
    ));

    webTestClient.post()
            .uri("/tasks")
            .header("Content-Type", "application/json")
            .bodyValue(incompleteTaskDto)
            .exchange()
            .expectStatus().isInternalServerError() // 修正为对应TE_005的状态码
            .expectBody()
            .jsonPath("$.success").isEqualTo(false)
            .jsonPath("$.data").isEmpty();

    verify(taskService, times(1)).create(any(Mono.class));
}

2. 代码优化建议

(1)统一错误处理逻辑

Handler和Service中重复的onErrorResume判断逻辑可抽成公共工具方法:

// HandlerUtils中新增方法
public static <T> Mono<T> wrapUnknownError(Throwable ex, TaskError errorCode) {
    log.error("Unexpected error: ", ex);
    if (ex instanceof TaskMgmtException) {
        return Mono.error(ex);
    }
    return Mono.error(new TaskMgmtException(errorCode));
}

使用时简化为:

// Handler中
.onErrorResume(ex -> HandlerUtils.wrapUnknownError(ex, TaskError.TE_004))

// Service中
.onErrorResume(ex -> HandlerUtils.wrapUnknownError(ex, TaskError.TE_005))

(2)简化对象转换

手动给Task赋值的代码冗余易出错,建议使用MapStruct等对象映射框架自动生成转换代码:

// 定义Mapper接口
@Mapper(componentModel = "spring")
public interface TaskMapper {
    TaskMapper INSTANCE = Mappers.getMapper(TaskMapper.class);

    Task toEntity(TaskDto dto);
    TaskDto toDto(Task entity);
}

Service中代码简化为:

.flatMap(task -> taskRepository.save(TaskMapper.INSTANCE.toEntity(task))
                               .map(TaskMapper.INSTANCE::toDto))

(3)封装响应构建逻辑

将CommonResponse的构建抽成工具方法,避免重复代码:

// ResponseUtils中新增方法
public static <T> CommonResponse<T> success(T data) {
    CommonResponse<T> response = new CommonResponse<>();
    response.setSuccess(true);
    response.setTimeStamp(LocalDateTime.now().toString());
    response.setData(data);
    return response;
}

Handler中使用:

.flatMap(taskDto -> ServerResponse
        .status(HttpStatus.CREATED)
        .contentType(MediaType.APPLICATION_JSON)
        .bodyValue(ResponseUtils.success(taskDto)))

(4)分离日志与错误处理

将日志逻辑从onErrorResume中分离,使用doOnError单独处理日志,让错误处理逻辑更清晰:

// Handler中示例
Mono<TaskDto> taskToCreate = request
        .bodyToMono(TaskDto.class)
        .flatMap(body -> HandlerUtils.validateAndReturn(body, validator))
        .doOnError(ex -> log.error("Error on createTask method: ", ex))
        .onErrorResume(ex -> HandlerUtils.wrapUnknownError(ex, TaskError.TE_004));

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 11:48:09