知識庫 / Spring / Spring Boot RSS 訂閱

禁用 Spring 測試中的 @EnableScheduling

Spring Boot,Testing
HongKong
4
11:15 AM · Dec 06 ,2025

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 註解,並基於環境別名或屬性值進行條件配置,是一種不錯的做法。如果無法實現,則可以設置高初始延遲,讓執行測試邏輯的任務運行。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.