在 Spring 測試中停用 @EnableScheduling
一、簡介
在本教程中,我們將深入探討使用排程任務測試 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] cbdDelayedNotificationScheduler : Scheduled notifications send out attempt
2024-03-13T00:17:38.637+01:00 INFO 4728 --- [pool-1-thread-1] cbdNotificationService : Sending out delayed notifications
2024-03-13T00:17:38.644+01:00 INFO 4728 --- [ main] cbdDelayedNotificationScheduler : Scheduled notifications send out attempt
2024-03-13T00:17:38.644+01:00 INFO 4728 --- [ main] cbdNotificationService : Sending out delayed notifications
2024-03-13T00:17:38.647+01:00 INFO 4728 --- [pool-1-thread-1] cbdDelayedNotificationScheduler : Scheduled notifications send out attempt
2024-03-13T00:17:38.647+01:00 INFO 4728 --- [pool-1-thread-1] cbdNotificationService : Sending out delayed notifications
分析輸出,我們可以發現attemptSendingOutDelayedNotifications()
方法已被多次呼叫。
一次呼叫來自main
線程,而其他呼叫來自pool-1-thread-1
。
我們可以觀察到這種行為,因為應用程式在啟動期間初始化了規劃任務。它們會定期在屬於單獨執行緒池的執行緒中呼叫我們的調度程序。這就是為什麼我們可以看到來自pool-1-thread-1
的方法呼叫。另一方面,來自main
線程的呼叫是我們在整合測試中直接呼叫的。
測試通過了,但該操作被調用了多次。這只是這裡的程式碼味道,但在不太幸運的情況下可能會導致不穩定的測試。我們的測試應該盡可能明確和隔離。因此,我們應該引入修復程序,讓我們放心,呼叫調度程序的唯一時間是我們直接呼叫它的時間。
4. 停用整合測試的排程任務
讓我們考慮一下我們可以做些什麼來確保在測試期間,只有我們想要執行的程式碼正在執行。我們將要採用的方法與允許我們有條件地在 Spring 應用程式中啟用排程作業的方法類似,但針對整合測試中的使用進行了調整。
4.1.根據Profile啟用@EnableScheduling
註解的配置
首先,我們可以將配置中啟用調度的部分提取到另一個配置類別。然後,我們可以根據活動設定檔有條件地套用它。在我們的例子中,我們希望在integrationTest
檔處於活動狀態時停用調度:
@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"
}
)
此設定允許我們確保在執行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
註釋提取到有條件應用的單獨配置來停用調度。當不可能時,我們始終可以為執行我們正在測試的邏輯的任務設定較高的初始延遲。
本文中提供的所有程式碼都可以在 GitHub 上取得。