1、简介 {#1简介}
本文将带你了解如何测试启用了定时任务(@EnableScheduling
)的 Spring 应用,以及如何在测试过程中禁用定时任务。
2、示例 {#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
配置的时间过去后,该方法将被首次调用。执行结束后,Spring 会在 fixedDelayString
参数配置的时间过后再次调用该方法。该方法本身将实际逻辑委托给了 NotificationService
。
当然,我们还需要开启调度功能。为此,需要在 @Configuration
类上添加 @EnableScheduling
注解。
3、集成测试中定时任务的问题 {#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
。
这是因为应用在启动过程中初始化了调度任务。它们会在属于独立线程池的线程中定期调用调定时任务。这就是为什么我们能看到来自 pool-1-thread-1
的方法调用。而,来自 main
线程的调用是我们在集成测试中直接调用的。
测试通过了,但任务被调用了多次。这只是一个代码问题,但在不太幸运的情况下,它可能会导致测试不稳定。我们的测试应该尽可能明确和隔离。因此,应该引入修复措施,确保调度程序只在我们直接调用它的时候被调用。
4、在集成测试中禁用定时任务 {#4在集成测试中禁用定时任务}
接下来看看在集成测试中禁用定时任务的一些方法,这些方法类似于允许我们在 Spring 应用中有条件地启用定时任务的方法,但针对集成测试中的使用进行了调整。
4.1、根据 Profile 启用 @EnableScheduling 配置类 {#41根据-profile-启用-enablescheduling-配置类}
首先,可以将配置中支持调度的部分提取到另一个配置类中。然后,可以根据激活的 Profile 有条件地应用它。在本例中,我们希望在 integrationTest Profile 处于激活状态时禁用调度:
@Configuration
@EnableScheduling
@Profile("!integrationTest") // 非 integrationTest Profile 时激活配置类
public class SchedulingConfig {
}
在集成测试中,唯一需要做的就是启用上述 Profile:
@SpringBootTest(
classes = { ApplicationConfig.class, SchedulingConfig.class, SchedulerTestConfiguration.class },
properties = {
"notification.send.out.delay: 10",
"notification.send.out.initial.delay: 0"
}
)
通过这种设置,可以确保在 elayedNotificationSchedulerIntegrationTest
中定义的所有测试执行期间,调度功能被禁用,任何定时任务都不会执行。
4.2、基于配置属性启用配 @EnableScheduling 配置类 {#42基于配置属性启用配-enablescheduling-配置类}
另一种类似的方法是根据属性值为应用启用调度。使用 @ConditionalOnProperty
注解根据不同的配置属性值来启用配置类:
@Configuration
@EnableScheduling
@ConditionalOnProperty(value = "scheduling.enabled", havingValue = "true", matchIfMissing = true)
public class SchedulingConfig {
}
现在,调度取决于 scheduling.enabled
属性的值。如果我们有意地将其设置为 false
,Spring 就不会加载 SchedulingConfig
配置类。
集成测试方面所需的更改微乎其微:
@SpringBootTest(
classes = { ApplicationConfig.class, SchedulingConfig.class, SchedulerTestConfiguration.class },
properties = {
"notification.send.out.delay: 10",
"notification.send.out.initial.delay: 0",
"scheduling.enabled: false"
}
)
4.3、微调定时任务配置 {#43微调定时任务配置}
最后,可以稍微修改一下定时任务的配置。可以为它们设置很长的初始延迟时间,这样集成测试在 Spring 尝试执行任何周期性操作之前就有足够的时间来执行:
@SpringBootTest(
classes = { ApplicationConfig.class, SchedulingConfig.class, SchedulerTestConfiguration.class },
properties = {
"notification.send.out.delay: 10",
"notification.send.out.initial.delay: 60000"
}
)
只需设置 60 秒的初始延迟。在不影响 Spring 管理的定时任务的情况下,集成测试应该有足够的时间通过。
不过,需要注意的是,这是在无法引入前述选项的情况下不得已而为之的做法。最好的做法是避免在代码中引入任何与时间相关的依赖。测试有时需要更长的时间来执行,这有很多原因,例如:过度使用的 CI 服务器。在这种情况下,项目中可能会出现不稳定的测试。
5、总结 {#5总结}
本文介绍了在集成测试中禁用 Spring 定时任务的几种方式。
Ref:https://www.baeldung.com/spring-test-disable-enablescheduling