
本文详解为何在使用 flatMap 递归调用 deleteAccount() 时出现 accountRegistry.getSessions() 被执行两次的问题,并指出根本原因在于测试中对同一 Mono 多次调用 block() 导致重复订阅,同时提供基于 StepVerifier 的正确异步测试方案与代码改进建议。
本文详解为何在使用 `flatmap` 递归调用 `deleteaccount()` 时出现 `accountregistry.getsessions()` 被执行两次的问题,并指出根本原因在于测试中对同一 `mono` 多次调用 `block()` 导致重复订阅,同时提供基于 `stepverifier` 的正确异步测试方案与代码改进建议。
在 Project Reactor 中,Mono 是一个冷发布者(cold publisher):每次订阅都会重新触发上游逻辑(如 HTTP 调用、数据库查询或服务方法执行)。这意味着,对同一个 Mono 实例多次调用 block()、subscribe() 或 toFuture().get(),将导致其内部链路被完整执行多次——这正是你观察到 accountRegistry.getSessions() 被验证“调用 2 次”的根本原因。
回顾你的测试代码:
var res = sut.deleteAccount(accountId); assertEquals(res.block().getDeletedSessions().contains(accountId)); // 第 1 次订阅 → 执行 getSessions() assertEquals(1, res.block().getDeletedSessions().size()); // 第 2 次订阅 → 再次执行 getSessions()! verify(accountRegistry).getSessions(List.of(accountId)); // Mockito 验证失败:期望 1 次,实际 2 次
⚠️ 关键问题:res.block() 被调用了两次 → deleteAccount() 对应的整个响应式链被触发两次 → accountRegistry.getSessions() 被执行两次。
✅ 正确做法:使用 StepVerifier 进行单次订阅验证
StepVerifier 是 Reactor 官方推荐的测试工具,它对目标 Mono 仅执行一次订阅,并通过声明式断言验证事件流(onNext、onComplete、error),彻底规避重复执行副作用的风险:
import reactor.test.StepVerifier;
@Test
void deleteAccount_shouldDeleteSessionOnceWhenActive() {
String accountId = "test-123";
// Arrange: stub dependencies if needed (e.g., accountDataProvider returns active account)
when(accountDataProvider.accountDataFor(accountId))
.thenReturn(Mono.just(new AccountData(true))); // isActive = true
// Act & Assert: single subscription, full flow verification
StepVerifier.create(sut.deleteAccount(accountId))
.expectNextMatches(response ->
response.getDeletedSessions().contains(accountId) &&
response.getDeletedSessions().size() == 1)
.expectComplete()
.verify();
// Now safely verify mock interaction — exactly once
verify(accountRegistry).getSessions(List.of(accountId));
}
? 补充建议:优化业务逻辑避免隐式递归风险
当前 flatMap 中的递归调用 deleteAccount(accountId) 存在潜在栈溢出与无限循环风险(若 accountData.isActive 始终为 true)。建议显式控制重试或转换为迭代式逻辑:
public Mono<response> deleteAccount(String accountId) {
return accountDataProvider.accountDataFor(accountId)
.flatMap(accountData -> {
if (!accountData.isActive) {
return Mono.just(new Response()); // short-circuit
}
// 执行删除逻辑(非递归)
return Mono.fromCallable(() -> {
var response = new Response();
accountRegistry.getSessions(List.of(accountId)).forEach(session ->
response.addToDeletedSessions(session)
);
return response;
}).subscribeOn(Schedulers.boundedElastic()); // I/O 操作建议指定线程池
});
}</response>
? 总结
- ❌ 错误实践:对同一
Mono多次调用block()→ 多次订阅 → 副作用重复执行; - ✅ 正确实践:使用
StepVerifier.create(mono).expectNext(...).verify()进行单次、可预测的流验证; - ? 提升健壮性:避免无保护的递归式
flatMap,优先使用声明式、可终止的逻辑分支; - ? 测试黄金法则:Reactor 测试 = 单次订阅 + 事件断言,而非阻塞式多点取值。
遵循以上原则,即可彻底解决“方法被调用两次”的问题,并构建真正符合响应式编程范式的可靠服务。











