giuseppe-trisciuoglio/unit-test-scheduled-async(Spring定时与异步方法单元测试)
提供针对 Spring @Scheduled 和 @Async 方法的单元测试模式,使用 JUnit 5、CompletableFuture、Awaitility 和 Mockito 进行测试。涵盖了模拟任务执行和时间控制、验证执行次数、测试 cron 表达式、验证重试行为以及模拟线程池行为。适用于测试后台任务、cron 作业、周期性执行、定时任务或线程池行为。
使用场景
- 测试 Spring 中的
@Scheduled方法(例如 cron 作业、固定延迟、固定速率)。 - 测试 Spring 中的
@Async方法(例如异步任务、Future 返回类型)。 - 验证任务执行次数和时序。
- 测试 cron 表达式逻辑。
- 验证重试机制(如
@Retryable)。 - 测试自定义线程池行为。
支持平台
- Spring Framework(5.x 及以上)
- Spring Boot(2.x 及以上)
- JUnit 5
- Mockito
- Awaitility
- CompletableFuture(Java 8+)
快速开始
1. 添加依赖
<!-- Maven 依赖示例 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<scope>test</scope>
</dependency>
2. 测试 @Scheduled 方法
import static org.mockito.Mockito.*;
import static org.awaitility.Awaitility.await;
@ExtendWith(MockitoExtension.class)
class ScheduledTaskTest {
@Mock
private SomeRepository repository;
private ScheduledTask task;
@BeforeEach
void setUp() {
task = new ScheduledTask(repository);
}
@Test
void shouldExecuteScheduledTask() {
// 模拟任务执行
task.run();
// 验证执行次数
verify(repository, times(1)).update();
}
@Test
void shouldWaitForAsyncCompletion() {
// 启动异步任务
CompletableFuture<Void> future = task.asyncOperation();
// 等待完成
await().atMost(5, SECONDS).until(future::isDone);
// 验证结果
verify(repository).save(any());
}
}
3. 测试 @Async 方法
@SpringBootTest
class AsyncTaskTest {
@Autowired
private AsyncTask asyncTask;
@Test
void shouldHandleMultipleAsyncCalls() throws Exception {
CompletableFuture<String> future1 = asyncTask.process();
CompletableFuture<String> future2 = asyncTask.process();
CompletableFuture.allOf(future1, future2).join();
assertEquals("完成", future1.get());
assertEquals("完成", future2.get());
}
}
常见模式
模拟任务执行时序
使用 Awaitility 等待特定条件:
await().atMost(10, SECONDS)
.until(() -> repository.updateCount() == 5);
测试 cron 表达式
@ParameterizedTest
@CsvSource({
"0 0 * * * *, 每小时执行",
"0 0 0 * * *, 每天午夜执行"
})
void shouldParseCronExpression(String cronExpression, String description) {
CronExpression cron = new CronExpression(cronExpression);
assertNotNull(cron.getNextValidTimeAfter(new Date()));
}
验证重试行为
@Test
void shouldRetryOnFailure() {
when(repository.save(any()))
.thenThrow(new RuntimeException("临时错误"))
.thenReturn("成功");
String result = task.retryableOperation();
assertEquals("成功", result);
verify(repository, times(2)).save(any());
}
模拟线程池行为
@Test
void shouldUseCustomThreadPool() {
ExecutorService executor = Executors.newFixedThreadPool(2);
task.setExecutor(executor);
// 执行任务
task.runWithExecutor();
// 验证执行在正确的线程中
assertTrue(executor.isShutdown());
}
最佳实践
- 隔离测试:避免启动完整 Spring 上下文,优先使用
@Mock和@InjectMocks。 - 显式等待:对于异步操作,始终使用 Awaitility 而非
Thread.sleep()。 - 模拟时间:使用
Clock或Datemock 来控制时间,而非依赖真实时间。 - 测试边界条件:测试 cron 表达式的边界值(例如每月的最后一天、闰年)。
- 验证异常处理:确保重试和错误处理逻辑已覆盖。
贡献
欢迎提交问题(issues)和拉取请求(pull requests)。请遵循现有代码风格和测试规范。
许可证
MIT