1. 引言
本文将深入探讨测试使用定时任务的Spring应用时可能遇到的问题。当项目中大量使用定时任务时,编写测试(尤其是集成测试)可能会成为开发者的噩梦。我们将介绍几种解决方案,确保测试的稳定性和可靠性。
2. 示例场景
假设我们有一个企业通知系统,允许公司代表向客户发送通知。其中部分通知需要立即发送,而部分则需要等到下一个工作日。因此我们需要一个定时任务机制来周期性处理这些通知:
public class DelayedNotificationScheduler {
private NotificationService notificationService;
@Scheduled(fixedDelayString = "${notification.send.out.delay}", initialDelayString = "${notification.send.out.initial.delay}")
public void attemptSendingOutDelayedNotifications() {
notificationService.sendOutDelayedNotifications();
}
}
关键点: attemptSendingOutDelayedNotifications()
方法使用了 @Scheduled
注解。该方法首次执行时间由 initialDelayString
控制,后续执行间隔由 fixedDelayString
控制。实际业务逻辑委托给了 NotificationService
。
要启用定时任务,**需要在 @Configuration
注解的类上添加 @EnableScheduling
**。虽然这是基础配置,但后续我们会看到如何避免它对测试产生负面影响。
3. 集成测试中的定时任务问题
先看一个基础的集成测试示例:
@SpringBootTest(
classes = { ApplicationConfig.class, SchedulerTestConfiguration.class },
properties = {
"notification.send.out.delay: 10",
"notification.send.out.initial.delay: 0"
}
)
public class DelayedNotificationSchedulerIntegrationTest {
@Autowired
private Clock testClock;
@Autowired
private NotificationRepository repository;
@Autowired
private DelayedNotificationScheduler scheduler;
@Test
public void whenTimeIsOverNotificationSendOutTime_thenItShouldBeSent() {
ZonedDateTime fiveMinutesAgo = ZonedDateTime.now(testClock).minusMinutes(5);
Notification notification = new Notification(fiveMinutesAgo);
repository.save(notification);
scheduler.attemptSendingOutDelayedNotifications();
Notification processedNotification = repository.findById(notification.getId());
assertTrue(processedNotification.isSentOut());
}
}
@TestConfiguration
class SchedulerTestConfiguration {
@Bean
@Primary
public Clock testClock() {
return Clock.fixed(Instant.parse("2024-03-10T10:15:30.00Z"), ZoneId.systemDefault());
}
}
注意:@EnableScheduling
直接应用在 ApplicationConfig
类上,该类负责创建测试中所有自动装配的Bean。
运行测试后观察日志输出:
2024-03-13T00:17:38.637+01:00 INFO 4728 --- [pool-1-thread-1] c.b.d.DelayedNotificationScheduler : Scheduled notifications send out attempt
2024-03-13T00:17:38.637+01:00 INFO 4728 --- [pool-1-thread-1] c.b.d.NotificationService : Sending out delayed notifications
2024-03-13T00:17:38.644+01:00 INFO 4728 --- [ main] c.b.d.DelayedNotificationScheduler : Scheduled notifications send out attempt
2024-03-13T00:17:38.644+01:00 INFO 4728 --- [ main] c.b.d.NotificationService : Sending out delayed notifications
2024-03-13T00:17:38.647+01:00 INFO 4728 --- [pool-1-thread-1] c.b.d.DelayedNotificationScheduler : Scheduled notifications send out attempt
2024-03-13T00:17:38.647+01:00 INFO 4728 --- [pool-1-thread-1] c.b.d.NotificationService : Sending out delayed notifications
⚠️ 问题分析:
attemptSendingOutDelayedNotifications()
被多次调用- 一次来自
main
线程(测试直接调用) - 多次来自
pool-1-thread-1
线程(Spring定时任务线程池)
虽然测试通过了,但方法被多次执行是个危险的信号。 在更复杂的场景下,这会导致测试不稳定。理想情况下,测试应该完全可控,只执行我们显式调用的代码。
4. 在集成测试中禁用定时任务
以下是几种确保测试隔离性的解决方案,与条件启用定时任务的思路类似,但专门针对测试场景优化。
4.1. 基于Profile条件启用配置
将 @EnableScheduling
抽取到独立配置类,并通过Profile控制加载。 例如在 integrationTest
激活时禁用定时任务:
@Configuration
@EnableScheduling
@Profile("!integrationTest")
public class SchedulingConfig {
}
测试端只需激活对应Profile:
@SpringBootTest(
classes = { ApplicationConfig.class, SchedulingConfig.class, SchedulerTestConfiguration.class },
properties = {
"notification.send.out.delay: 10",
"notification.send.out.initial.delay: 0"
}
)
@ActiveProfiles("integrationTest")
✅ 效果: 所有标记 @ActiveProfiles("integrationTest")
的测试类都会自动禁用定时任务。
4.2. 基于属性条件启用配置
另一种方案是通过属性值控制配置加载。 修改配置类如下:
@Configuration
@EnableScheduling
@ConditionalOnProperty(value = "scheduling.enabled", havingValue = "true", matchIfMissing = true)
public class SchedulingConfig {
}
现在定时任务依赖 scheduling.enabled
属性。测试中只需禁用该属性:
@SpringBootTest(
classes = { ApplicationConfig.class, SchedulingConfig.class, SchedulerTestConfiguration.class },
properties = {
"notification.send.out.delay: 10",
"notification.send.out.initial.delay: 0",
"scheduling.enabled: false"
}
)
✅ 效果: 与Profile方案效果相同,但更适合需要动态控制的场景。
4.3. 调整定时任务配置参数
最后一种方案是延长定时任务的初始延迟时间,确保测试执行完成前Spring不会触发定时任务:
@SpringBootTest(
classes = { ApplicationConfig.class, SchedulingConfig.class, SchedulerTestConfiguration.class },
properties = {
"notification.send.out.delay: 10",
"notification.send.out.initial.delay: 60000" // 60秒延迟
}
)
❌ 缺点:
- 这是最后的无奈之选,当前两种方案不可用时才考虑
- 引入了时间依赖,可能导致测试不稳定
- 在CI服务器负载高时容易失败
- 违背了测试应该快速、可靠的原则
⚠️ 建议: 除非有特殊约束,否则优先使用前两种方案。
5. 总结
本文探讨了在测试使用定时任务的Spring应用时的几种配置方案:
核心问题: 让定时任务与集成测试同时运行会导致测试不稳定(flaky tests)。
推荐方案:
- ✅ 将
@EnableScheduling
抽取到独立配置类 - ✅ 通过Profile或属性条件控制加载
- ❌ 避免使用延长延迟时间的方案(除非别无选择)
最佳实践: 测试环境应完全隔离定时任务的自动执行,确保测试结果可预测、可重复。所有示例代码可在GitHub仓库中获取。