CompletableFuture.runAsync()
是 Java 中用于异步执行无返回值任务的常用方法。由于其异步特性,测试起来可能会有些棘手,因为你需要确保任务完成后再验证结果。本文将介绍几种测试 CompletableFuture.runAsync()
的方法,从简单到高效,帮助你写出健壮的单元测试。
示例代码
假设我们有以下使用 CompletableFuture.runAsync()
的服务类:
@RequiredArgsConstructor
public class AsyncService {
private final LoggerService loggerService;
public void runTask() {
CompletableFuture.runAsync(() -> loggerService.log("Task executed asynchronously"));
}
}
interface LoggerService {
void log(String message);
}
这个类通过 runAsync()
异步调用 loggerService.log()
方法。接下来,我们需要测试 runTask()
是否正确触发了日志记录。
方法 1:直接调用并验证(不推荐)
最简单的测试方法是直接调用方法,然后验证 mock 的行为:
@Test
void testRunTaskDirectly() {
LoggerService loggerService = mock(LoggerService.class);
AsyncService asyncService = new AsyncService(loggerService);
asyncService.runTask();
verify(loggerService).log("Task executed asynchronously");
}
问题:
由于 runAsync()
是异步的,测试线程可能在任务完成前就执行了 verify()
,导致测试偶尔失败。这种方法依赖线程调度的运气,非常不可靠。
方法 2:使用 Thread.sleep()
(简单但低效)
为了确保异步任务完成,可以添加 Thread.sleep()
等待一段时间:
@Test
void testRunTaskWithSleep() throws InterruptedException {
LoggerService loggerService = mock(LoggerService.class);
AsyncService asyncService = new AsyncService(loggerService);
asyncService.runTask();
Thread.sleep(500); // 等待 500 毫秒
verify(loggerService).log("Task executed asynchronously");
}
优点:
• 简单易懂,能解决问题。
缺点:
• 效率低,增加了测试时间。
• 等待时间不好确定,太短可能不够,太长浪费时间。
• 不够优雅,属于“硬等”方案。
方法 3:使用 CompletableFuture.join()
(同步等待)
由于 CompletableFuture.runAsync()
返回一个 CompletableFuture<Void>
,我们可以将其返回值暴露出来,然后用 join()
等待任务完成:
@RequiredArgsConstructor
public class AsyncService {
private final LoggerService loggerService;
// 修改方法以返回 CompletableFuture
public CompletableFuture<Void> runTask() {
return CompletableFuture.runAsync(() -> loggerService.log("Task executed asynchronously"));
}
}
@Test
void testRunTaskWithJoin() {
LoggerService loggerService = mock(LoggerService.class);
AsyncService asyncService = new AsyncService(loggerService);
CompletableFuture<Void> future = asyncService.runTask();
future.join(); // 等待任务完成
verify(loggerService).log("Task executed asynchronously");
}
优点:
• 精确等待任务完成,不用猜时间。
• 比
Thread.sleep()
更可靠。
缺点:
• 需要修改原方法签名,返回
CompletableFuture
,可能不适合所有场景。• 测试代码侵入性稍高。
方法 4:使用 Awaitility
(推荐)
Awaitility
是一个专门用于测试异步代码的库,优雅且高效。只需要添加依赖:
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<version>4.2.0</version>
<scope>test</scope>
</dependency>
然后用它来等待并验证:
@Test
void testRunTaskWithAwaitility() {
LoggerService loggerService = mock(LoggerService.class);
AsyncService asyncService = new AsyncService(loggerService);
asyncService.runTask();
await()
.atMost(Duration.ofSeconds(1)) // 最多等待 1 秒
.untilAsserted(() -> verify(loggerService).log("Task executed asynchronously"));
}
优点:
• 无需修改原始代码,保持方法签名不变。
• 灵活配置等待时间和条件,测试更健壮。
• 优雅且专为异步场景设计。
缺点:
• 需要引入额外依赖,但测试收益远超成本。
方法 5:使用 CountDownLatch
(手动同步)
如果不想引入额外库,可以用 CountDownLatch
实现同步:
@Test
void testRunTaskWithLatch() throws InterruptedException {
LoggerService loggerService = mock(LoggerService.class);
CountDownLatch latch = new CountDownLatch(1);
// 包装 LoggerService 以触发 latch
doAnswer(invocation -> {
loggerService.log(invocation.getArgument(0));
latch.countDown();
return null;
}).when(loggerService).log(anyString());
AsyncService asyncService = new AsyncService(loggerService);
asyncService.runTask();
latch.await(1, TimeUnit.SECONDS); // 等待最多 1 秒
verify(loggerService).log("Task executed asynchronously");
}
优点:
• 不依赖外部库,纯 Java 实现。
• 精确控制同步点。
缺点:
• 代码复杂,需手动管理
CountDownLatch
。• 测试代码侵入性较高。
总结与推荐
测试 CompletableFuture.runAsync()
的关键是处理异步执行的不确定性。以下是各方法的适用场景:
方法 | 优点 | 缺点 | 推荐场景 |
直接调用 | 简单 | 不稳定,易失败 | 不推荐 |
Thread.sleep() | 简单,可行 | 低效,时间难把握 | 临时调试 |
join() | 精确,内置支持 | 需改代码 | 可改签名的小项目 |
Awaitility | 优雅,灵活,无侵入 | 需加依赖 | 大多数生产级项目 |
CountDownLatch | 无依赖,精确 | 代码复杂 | 不想加依赖的场景 |
推荐:
首选 Awaitility
,它专为异步测试设计,既优雅又高效。如果无法引入依赖,join()
是次优选择,前提是你能修改方法签名。对于快速验证,Thread.sleep()
也能凑合,但不适合长期使用。
希望这些方法能帮你在测试 CompletableFuture.runAsync()
时游刃有余!有什么问题,欢迎讨论。