关于 unit-test-scheduled-async
面向使用 Spring 和 JUnit 5 的开发者,介绍隔离测试 `@Scheduled` 与 `@Async` 方法的模式,涵盖异步结果、异常和无需等待真实调度的验证。
主要功能
- 01直接调用待测方法
- 02验证 CompletableFuture 结果
- 03使用 Awaitility 等待条件
- 04测试异步异常传播
- 05验证任务执行次数
适用场景
- 01测试定时任务逻辑
- 02检查异步方法返回值
- 03验证异步异常处理
- 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 表达式逻辑
- 验证线程池行为和执行次数
- 隔离测试后台任务逻辑
说明
- 直接调用
@Async方法——绕过 Spring 的异步代理;在单元测试中,该注解不起作用 - 使用
@Mock和@InjectMocks(Mockito)模拟依赖项 - 等待执行完成——使用
CompletableFuture.get(timeout, unit)或await().atMost(...).untilAsserted(...) - 直接调用
@Scheduled方法——不要等待 cron/fixedRate;在单元测试中,该注解会被忽略 - 测试异常路径——验证
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会被忽略——请直接调用方法
参考资料
- Spring
@Async文档 - Spring
@Scheduled文档 - Awaitility 测试库
- CompletableFuture API
- 代码示例:
references/examples.md
评论
支持 Markdown