跳转到主内容
极星编程网:以代码为星,赴技术山海!

如何在响应式编程中正确测试 onErrorResume 逻辑

本文详解如何为 Reactor 的 onErrorResume 操作符编写可靠单元测试,重点解决“Mock 异步错误不触发恢复逻辑”的常见误区,并提供基于 StepVerifier 和 blockOptional() 的验证方案,同时给出更健壮的异常分类处理建议。 本文详解如何为 reactor 的 `onerrorresume` 操作符编写可靠单元测试,重点解决“mock 异步错误不触发恢复逻辑”的常见误区,并提供基于 `stepverifier` 和 `blockoptional()` 的验证方案,同时给出更健壮的异常分类处理建议。 在 响应式编程 中,onErrorResume 是核心错误恢复操作符,但其测试方式与命令式代码有本质区别: 它只对 Mono/Flux 的 error 信号生效,而非抛出的同步异常 。因此,直接使用 doThrow(...) 模拟 Web 客户端异常会导致测试失败——因为 getResponse() 返回 null 或未被订阅的空 Mono,错误根本未进入响应式流,onErrorResume 自然不会执行。 ✅ 正确做法是: 让 mock 方法返回一个 Mono.error(...) ,确保错误作为 Reactive Stream 的 signal 被下游操作符捕获:
// ✅ 正确:返回错误 Mono,触发 onErrorResume doReturn(Mono.error(new WebClientResponseException(400, "Bad Request", null, null, null))) .when(myWebClient) .getResponse(anyString());
⚠️ 注意:Mono.error(() -> exception) 是惰性求值写法(推荐用于避免提前构造异常),但 Mono.error(exception) 同样有效且更简洁;二者均能正确发射 error 信号。 推荐测试方案:使用 StepVerifier(最佳实践) StepVerifier 是 Project Reactor 官方推荐的响应式流断言工具,支持完整生命周期验证(包括 error、empty、timeout 等),语义清晰且线程安全:
@Test void testOnErrorResumeForBadRequest() { // Arrange MyWebClient mockWebClient = mock(MyWebClient.class); doReturn(Mono.error(new WebClientResponseException(400, "Bad Request", null, null, null))) .when(mockWebClient) .getResponse(anyString()); MyService service = new MyService(mockWebClient); // 注入 mock // Act & Assert StepVerifier.create(service.myServiceMethod()) .verifyComplete(); // 因 onErrorResume 返回 Mono.empty(),最终流完成且无元素 }
若需验证 onErrorResume 中的具体分支逻辑(如日志、外部调用),可结合 doOnNext/doOnError + StepVerifier.verifyThenAssertThat(),或在 onErrorResume 内部插入 log() 或 doOnNext 辅助调试。 替代方案:使用 blockOptional(适用于简单场景) 对于快速验证结果是否为空,可使用阻塞式断言(仅限测试, 禁止在生产代码中调用 block() ):
@Test void testOnErrorResumeWithBlock() { MyWebClient mockWebClient = mock(MyWebClient.class); doReturn(Mono.error(new WebClientResponseException(404, "Not Found", null, null, null))) .when(mockWebClient) .getResponse(anyString()); Mono result = myService(myWebClient).myServiceMethod(); // 验证 onErrorResume 返回 Mono.empty() → blockOptional 为空 assertThat(result.blockOptional()).isEmpty(); }
进阶建议:重构更健壮的错误处理逻辑 原代码依赖 ex.getMessage().contains("400") 判断状态码存在严重风险(消息内容不稳定、国际化影响、误匹配)。应优先利用 WebClientResponseException 的子类型或状态码属性:
return responseDataMono .onErrorResume(WebClientResponseException.NotFound.class, err -> { log.warn("Student not found: {}", err.getStatusCode()); return Mono.empty(); // 或 fallback to default student }) .onErrorResume(WebClientResponseException.BadRequest.class, err -> { log.error("Invalid request for student: {}", err.getResponseBodyAsString()); return Mono.empty(); }) .onErrorResume(TimeoutException.class, err -> { log.warn("Request timeout", err); return Mono.empty(); }) .onErrorResume(WebClientResponseException.class, err -> { log.error("Unexpected WebClient error: {}", err.getStatusCode(), err); return Mono.empty(); }) .map(resp -> resp);
此写法类型安全、语义明确、易于测试,且避免字符串解析陷阱。 ✅ 总结:测试 onErrorResume 的关键三步: Mock 返回 Mono.error(...) ,而非 doThrow(...); 使用 StepVerifier.create(...).verifyComplete() / .verifyError(...) 等声明式断言 ; 重构错误分类逻辑,优先使用异常类型或状态码,而非消息文本匹配 。

相关文章