异步代码怎么测?五招搞定 CompletableFuture.runAsync()

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() 时游刃有余!有什么问题,欢迎讨论。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

java干货

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值