如何测试订阅 Uni 的 void 方法

本文介绍在 quarkus 或 mutiny 环境下,如何可靠地测试那些直接订阅 uni 并执行副作用(如日志记录)但不返回响应的 void 方法,解决因异步执行导致的竞态条件问题。

在使用 Mutiny 的响应式编程实践中,一个常见痛点是:当业务方法(如 execute())内部调用 uni.subscribe().with(...) 并仅执行副作用(例如打日志、更新状态),而自身返回 void 时,该方法在单元测试中极易因异步调度而出现竞态失败——断言在 Uni 实际完成前就已执行,导致 Mockito.verify() 检查失败。

直接使用 Thread.sleep(1000)(如答案中所示)虽能“凑效”,但属于反模式:它使测试变慢、不可靠(时间阈值难适配不同环境)、且掩盖了设计缺陷。更专业、可维护的解决方案应兼顾即时性、确定性与可测性

✅ 推荐方案一:注入可控的 Uni + 使用 awaitility(推荐)

Awaitility 是专为异步断言设计的轻量库,支持声明式等待条件,语义清晰、超时可控、线程安全:



  org.awaitility
  awaitility
  test

测试示例:

@Test
public void shouldLogSuccessAfterReprocessing() {
    // 给 service 注入一个立即完成的 Uni(例如通过 Mockito mock)
    Uni completedUni = Uni.createFrom().voidItem();
    when(service.reprocessAll()).thenReturn(completedUni);

    service.execute();

    // 等待日志被调用一次,最多等待 3 秒,每 100ms 检查一次
    await()
  

.atMost(3, TimeUnit.SECONDS) .pollInterval(100, TimeUnit.MILLISECONDS) .untilAsserted(() -> Mockito.verify(log, times(1)).info("Reprocessing ran successfully.") ); }

⚠️ 注意:确保 log 是被 @Mock 的真实 mock 对象,且 service 是通过依赖注入或构造器注入的可替换实例。

✅ 推荐方案二:重构为可组合接口(长期最佳实践)

根本解法是将副作用分离并返回可测试的信号。修改 execute() 使其返回 Uni,而非 void:

public Uni execute() {
    return service.reprocessAll()
        .onItem().invoke(v -> log.info("Reprocessing ran successfully."))
        .onFailure().invoke(t -> log.severe("Reprocessing failed: " + t.getMessage()))
        .replaceWithVoid(); // 返回 Uni 表示“执行完成”
}

此时测试变得简洁、同步、无竞态:

@Test
public void shouldLogSuccessAfterReprocessing() {
    when(service.reprocessAll()).thenReturn(Uni.createFrom().voidItem());

    service.execute()
        .subscribe()
        .withSubscriber(UniAssertSubscriber.create())
        .assertCompleted();

    Mockito.verify(log, times(1)).info("Reprocessing ran successfully.");
}

? 总结

  • ❌ 避免 Thread.sleep():非确定性、低效、易误判;
  • ✅ 优先用 Awaitility:适合短期适配遗留 void 方法;
  • ✅ 更优是重构为返回 Uni:符合响应式契约,天然可链式断言,提升代码内聚性与可观测性;
  • ? 测试前提:确保被测对象依赖可 mock(如 service, log),并采用真正的异步执行环境(如 Mutiny 默认的 io.smallrye.mutiny.infrastructure.Infrastructure.getDefaultExecutor())。

通过以上方式,你既能快速修复当前测试失败,又能逐步演进代码向更健壮、可维护的响应式设计靠拢。