1. 简介

Spring Framework 提供了一个非常实用的注解 @Scheduled,可以方便地实现定时任务。通过它,我们可以按照固定频率、延迟或 cron 表达式来执行方法。

本篇文章将带你了解如何对使用了 @Scheduled 注解的方法进行测试。

2. 依赖配置

首先我们创建一个基于 Maven 的 Spring Boot 工程。可以从 Spring Initializer 快速生成模板项目。

pom.xml 中添加必要的依赖:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <relativePath/>
</parent>

接着引入两个核心 starter:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

同时加上 JUnit 5 的依赖:

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
</dependency>

最后,为了支持异步等待断言(后面会用到),我们还需要引入 Awaitility:

<dependency>
    <groupId>org.awaitility</groupId>
    <artifactId>awaitility</artifactId>
    <version>3.1.6</version>
    <scope>test</scope>
</dependency>

✅ 最新版 Spring Boot 可以去 Maven Central 查找。

3. 一个简单的 @Scheduled 示例

我们先定义一个简单的计数器类 Counter

@Component
public class Counter {
    private AtomicInteger count = new AtomicInteger(0);

    @Scheduled(fixedDelay = 5)
    public void scheduled() {
        this.count.incrementAndGet();
    }

    public int getInvocationCount() {
        return this.count.get();
    }
}

这个类中有一个被 @Scheduled(fixedDelay = 5) 标记的方法,表示每次执行完后延迟 5 毫秒再次执行。

再写一个配置类启用调度功能:

@Configuration
@EnableScheduling
@ComponentScan("com.baeldung.scheduled")
public class ScheduledConfig {
}

⚠️ 注意:一定要加上 @EnableScheduling 才能让定时任务生效!

4. 使用集成测试方式验证

最直观的方式就是启动 Spring 上下文,让定时任务真正跑起来,然后验证其行为。

我们可以使用 @SpringJUnitConfig 来加载配置类并启动容器:

@SpringJUnitConfig(ScheduledConfig.class)
public class ScheduledIntegrationTest {

    @Autowired 
    Counter counter;

    @Test
    public void givenSleepBy100ms_whenGetInvocationCount_thenIsGreaterThanZero() 
      throws InterruptedException {
        Thread.sleep(100L);

        assertThat(counter.getInvocationCount()).isGreaterThan(0);
    }
}

📌 这个测试逻辑很简单粗暴:

  • 启动上下文
  • 等待 100 毫秒
  • 验证计数器是否大于 0

虽然这种方式有效,但缺点也很明显:测试时间长,容易受环境影响。

5. 使用 Awaitility 更优雅地测试

Awaitility 是一个专门用于异步系统测试的库,可以让我们的测试代码更清晰、更具可读性。

来看下面这个例子:

@SpringJUnitConfig(ScheduledConfig.class)
public class ScheduledAwaitilityIntegrationTest {

    @SpyBean 
    private Counter counter;

    @Test
    public void whenWaitOneSecond_thenScheduledIsCalledAtLeastTenTimes() {
        await()
          .atMost(Duration.ONE_SECOND)
          .untilAsserted(() -> verify(counter, atLeast(10)).scheduled());
    }
}

💡 解释一下关键点:

  • 使用 @SpyBean 注入目标 Bean,这样就能 mock 并验证方法调用次数
  • await().atMost(Duration.ONE_SECOND) 表示最多等待 1 秒钟
  • verify(counter, atLeast(10)) 验证 scheduled() 方法至少被调用了 10 次

这种方式比起硬编码 sleep 更加灵活和准确。

6. 小结

本文介绍了两种常见的测试 @Scheduled 定时任务的方式:

集成测试:适合验证整个流程是否正常运行
Awaitility + SpyBean:更适合做细粒度的行为验证

不过需要提醒的是:虽然这些方法能验证定时任务是否触发,但更重要的是单元测试定时方法内部的业务逻辑,这才是测试的重点所在。

📌 示例代码可以从 GitHub 获取:https://github.com/eugenp/tutorials/tree/master/testing-modules/spring-testing


原始标题:How to Test the @Scheduled Annotation | Baeldung