WebTestClient链式Mono.onErrorResume行为异常,单元测试与Postman结果不符
问题背景
因工作需求接触响应式编程,正在开发首个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

