mcpField
U
unit-test-scheduled-async

unit-test-scheduled-async

github.com/giuseppe-trisciuoglio测试与质量3653.7k 次安装7 周前更新
代码仓库

关于 unit-test-scheduled-async

面向使用 Spring 和 JUnit 5 的开发者,介绍隔离测试 `@Scheduled` 与 `@Async` 方法的模式,涵盖异步结果、异常和无需等待真实调度的验证。

主要功能

  1. 01直接调用待测方法
  2. 02验证 CompletableFuture 结果
  3. 03使用 Awaitility 等待条件
  4. 04测试异步异常传播
  5. 05验证任务执行次数

适用场景

  1. 01测试定时任务逻辑
  2. 02检查异步方法返回值
  3. 03验证异步异常处理
  4. 04测试 cron 逻辑而不等待调度

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit --skill unit-test-scheduled-async

安装后在 Claude Code 中输入/unit-test-scheduled-async确认已加载。

附带文件references/examples.md

详细介绍

对 @Scheduled 和 @Async 方法进行单元测试

概述

介绍如何使用 JUnit 5 对 Spring 的 @Scheduled 和 @Async 方法进行单元测试。测试 CompletableFuture 结果,使用 Awaitility 处理竞态条件,模拟定时任务执行,并验证错误处理,无需等待实际的调度间隔。

适用场景

  • 测试 @Scheduled 方法逻辑
  • 测试 @Async 方法行为
  • 验证 CompletableFuture 结果
  • 测试异步错误处理
  • 无需等待实际调度即可测试 cron 表达式逻辑
  • 验证线程池行为和执行次数
  • 隔离测试后台任务逻辑

说明

  1. 直接调用 @Async 方法——绕过 Spring 的异步代理;在单元测试中,该注解不起作用
  2. 使用 @Mock 和 @InjectMocks(Mockito)模拟依赖项
  3. 等待执行完成——使用 CompletableFuture.get(timeout, unit) 或 await().atMost(...).untilAsserted(...)
  4. 直接调用 @Scheduled 方法——不要等待 cron/fixedRate;在单元测试中,该注解会被忽略
  5. 测试异常路径——验证 CompletableFuture.get() 对 ExecutionException 的包装

验证检查点:

  • 调用 CompletableFuture.get() 后,先断言返回值,再验证模拟对象的交互
  • 如果抛出 ExecutionException,检查 .getCause() 以确定根本异常
  • 如果 Awaitility 超时,请延长 atMost() 时长或缩短 pollInterval(),直到条件可达
  • 多次调用任务后,先断言执行次数,再调用 verify()

示例

关键模式——完整示例请参阅 references/examples.md:

// @Async: call directly, wait with CompletableFuture.get(timeout, unit)
@Service
class EmailService {
  @Async
  public CompletableFuture<Boolean> sendEmailAsync(String to) {
    return CompletableFuture.supplyAsync(() -> true);
  }
}
@Test
void shouldReturnCompletedFuture() throws Exception {
  EmailService service = new EmailService();
  Boolean result = service.sendEmailAsync("[email protected]").get(5, TimeUnit.SECONDS);
  assertThat(result).isTrue();
}

// @Scheduled: call directly, mock the repository
@Component
class DataRefreshTask {
  @InjectMocks private DataRepository dataRepository;
  @Scheduled(fixedDelay = 60000) public void refreshCache() { /* ... */ }
}
@Test
void shouldRefreshCache() {
  when(dataRepository.findAll()).thenReturn(List.of(new Data(1L, "item1")));
  dataRefreshTask.refreshCache();
  verify(dataRepository).findAll();
}

// Awaitility: use for race conditions with shared mutable state
@Test
void shouldProcessAllItems() {
  BackgroundWorker worker = new BackgroundWorker();
  worker.processItems(List.of("item1", "item2", "item3"));
  Awaitility.await()
    .atMost(Duration.ofSeconds(5))
    .pollInterval(Duration.ofMillis(100))
    .untilAsserted(() -> assertThat(worker.getProcessedCount()).isEqualTo(3));
}

// Mocked dependencies with exception handling
@Test
void shouldHandleAsyncExceptionGracefully() {
  doThrow(new RuntimeException("Email failed")).when(emailService).send(any());
  CompletableFuture<String> result = service.notifyUserAsync("user123");
  assertThatThrownBy(result::get)
    .isInstanceOf(ExecutionException.class)
    .hasCauseInstanceOf(RuntimeException.class);
}

完整的 Maven/Gradle 依赖项、其他测试类和执行次数模式,请参阅 references/examples.md。

最佳实践

  • 始终为 CompletableFuture.get() 设置超时时间,防止测试挂起
  • 模拟所有依赖项——单元测试中切勿调用真实的外部服务
  • 仅在存在竞态条件时使用 Awaitility;简单的异步方法应优先直接调用
  • 直接测试 @Scheduled 逻辑——在单元测试中,该注解会被忽略
  • 先断言值,再验证模拟对象的交互;在异步执行完成后进行验证

常见问题

  • 依赖 Spring 的异步执行器,而不是直接调用方法
  • 未为 CompletableFuture.get() 设置超时时间
  • 忘记测试异步方法中的异常传播
  • 未模拟异步方法内部调用的依赖项
  • 等待实际的 cron/fixedRate 时间,而不是隔离测试逻辑

限制和警告

  • @Async 自调用:在同一个类的另一个方法中调用 @Async 方法时,会同步执行,因为这绕过了 Spring 代理
  • 线程池顺序:ThreadPoolTaskScheduler 不保证执行顺序
  • CompletableFuture 链式调用:中间阶段的异常可能会被悄然丢弃——请测试每个阶段
  • Awaitility 超时:始终设置合理的 atMost();无限等待会导致测试套件挂起
  • 不执行实际调度:在单元测试中,@Scheduled 会被忽略——请直接调用方法

参考资料

评论

支持 Markdown