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 配置的時間過去後首次被調用。執行結束後,Spring 會在 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 線程。
我們觀察到這種行為是因為應用程序在啓動時初始化了計劃任務。這些任務週期性地調用我們的調度器,這些調用在屬於獨立線程池的線程上進行。因此,我們觀察到來自 pool-1-thread-1 的方法調用。另一方面,來自 main 線程的調用是我們在集成測試中直接調用的。
測試通過,但該動作被多次調用。這只是一個代碼異味,但在不太幸運的情況下,它可能會導致測試不穩定。 我們的測試應該儘可能明確和隔離。因此,我們應該引入一個修復方案,確保調度器僅在直接調用它時被調用。
4. 取消集成測試中計劃任務的執行
為了確保測試期間僅執行我們希望執行的代碼,我們將採取類似允許我們在 Spring 應用中條件啓用計劃任務的方法,但調整為適用於集成測試。
4.1. 基於 Profile 的啓用配置,並帶有 @EnableScheduling 註解
首先,我們可以將啓用定時任務的配置部分提取到另一個配置類中。然後,我們可以根據激活的 Profile 條件地應用它。 在我們的例子中,我們希望在 integrationTest Profile 激活時禁用定時任務:
@Configuration
@EnableScheduling
@Profile("!integrationTest")
public class SchedulingConfig {
}在集成測試方面,我們唯一需要做的就是啓用所提及的配置文件:
@SpringBootTest(
classes = { ApplicationConfig.class, SchedulingConfig.class, SchedulerTestConfiguration.class },
properties = {
"notification.send.out.delay: 10",
"notification.send.out.initial.delay: 0"
}
)
@ActiveProfiles("integrationTest")此配置允許我們確保在DelayedNotificationSchedulerIntegrationTest中所有定義的測試執行期間,調度功能被禁用,並且不會自動作為計劃任務執行任何代碼。
4.2. 通過基於屬性的 @EnableScheduling 進行配置啓用
另一種方法是基於屬性值啓用應用程序的調度。我們可以使用已提取的配置類並根據不同條件進行應用:
@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. 調整計劃任務配置
我們可以採取的最後一種方法是仔細調整計劃任務的配置。我們可以為它們設置非常長的初始延遲時間,以便集成測試有足夠的時間在 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. 結論
在本文中,我們探討了配置集成測試的各種選項,同時測試使用計劃任務機制的應用。
我們展示了僅僅讓計劃器與集成測試同時運行的後果。這會導致測試不穩定。
接下來,我們研究瞭如何確保計劃不會對測試產生負面影響。通過從配置中提取 @EnableScheduling 註解,並基於環境別名或屬性值進行條件配置,是一種不錯的做法。如果無法實現,則可以設置高初始延遲,讓執行測試邏輯的任務運行。