本文详解如何为 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.error(() -> exception) 是惰性求值写法(推荐用于避免提前构造异常),但 Mono.error(exception) 同样有效且更简洁;二者均能正确发射 error 信号。
推荐测试方案:使用 StepVerifier(最佳实践)
StepVerifier 是 Project Reactor 官方推荐的响应式流断言工具,支持完整生命周期验证(包括 error、empty、timeout 等),语义清晰且线程安全:
若需验证 onErrorResume 中的具体分支逻辑(如日志、外部调用),可结合 doOnNext/doOnError + StepVerifier.verifyThenAssertThat(),或在 onErrorResume 内部插入 log() 或 doOnNext 辅助调试。
替代方案:使用 blockOptional(适用于简单场景)
对于快速验证结果是否为空,可使用阻塞式断言(仅限测试,
禁止在生产代码中调用 block()
):
进阶建议:重构更健壮的错误处理逻辑
原代码依赖 ex.getMessage().contains("400") 判断状态码存在严重风险(消息内容不稳定、国际化影响、误匹配)。应优先利用 WebClientResponseException 的子类型或状态码属性:
此写法类型安全、语义明确、易于测试,且避免字符串解析陷阱。
✅ 总结:测试 onErrorResume 的关键三步:
Mock 返回 Mono.error(...)
,而非 doThrow(...);
使用 StepVerifier.create(...).verifyComplete() / .verifyError(...) 等声明式断言
;
重构错误分类逻辑,优先使用异常类型或状态码,而非消息文本匹配
。
// ✅ 正确:返回错误 Mono,触发 onErrorResume
doReturn(Mono.error(new WebClientResponseException(400, "Bad Request", null, null, null)))
.when(myWebClient)
.getResponse(anyString());@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(),最终流完成且无元素
}@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();
} 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);