我该如何在我的Spring Boot应用程序中测试@Scheduled
作业任务?
package com.myco.tasks;
public class MyTask {
@Scheduled(fixedRate=1000)
public void work() {
// task execution logic
}
}
我该如何在我的Spring Boot应用程序中测试@Scheduled
作业任务?
package com.myco.tasks;
public class MyTask {
@Scheduled(fixedRate=1000)
public void work() {
// task execution logic
}
}
如果我们假设你的工作运行在非常短的时间间隔内,你确实希望测试等待作业执行,并且只想测试作业是否被调用,那么可以使用以下解决方案:
将Awaitility添加到类路径中:
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<version>3.1.0</version>
<scope>test</scope>
</dependency>
编写类似于以下测试:
@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoApplicationTests {
@SpyBean
private MyTask myTask;
@Test
public void jobRuns() {
await().atMost(Duration.FIVE_SECONDS)
.untilAsserted(() -> verify(myTask, times(1)).work());
}
}
verify()
和times()
函数找不到。您能指定包名吗? - LiTTleorg.mockito.Mockito#verify
,times
同理。 - Maciej Walkowiak我的问题是:“你想测试什么?”
如果你的答案是“我想知道Spring在我想要的时候运行我的定时任务”,那么你正在测试Spring而不是你的代码。 这不是你需要进行单元测试的内容。
如果你的答案是“我想知道我正确地配置了我的任务”,那么请编写一个频繁运行任务的测试应用程序,并验证任务在你期望的时间运行。 这不是一个单元测试,但将显示您知道如何正确配置您的任务。
如果答案是“我想知道我编写的任务功能是否正确”,那么您需要对任务方法进行单元测试。
在你的例子中,你想对work()
方法进行单元测试。
通过编写直接调用任务方法(work()
)的单元测试来实现这一点。
例如:
public class TestMyTask
{
@InjectMocks
private MyTask classToTest;
// Declare any mocks you need.
@Mock
private Blammy mockBlammy;
@Before
public void preTestSetup()
{
MockitoAnnotations.initMocks(this);
... any other setup you need.
}
@Test
public void work_success()
{
... setup for the test.
classToTest.work();
.. asserts to verify that the work method functioned correctly.
}
@Maciej提供的答案解决了问题,但未解决测试@Scheduled使用过长间隔(例如几小时)的难点,正如@cristian-batista所提到的。
为了独立于实际调度间隔测试@Scheduled,我们需要从测试中使其可参数化。幸运的是,Spring添加了一个fixedRateString
参数用于此目的。
以下是一个完整的示例:
public class MyTask {
// Control rate with property `task.work.rate` and use 3600000 (1 hour) as a default:
@Scheduled(fixedRateString = "${task.work.rate:3600000}")
public void work() {
// task execution logic
}
}
使用awaitility进行测试:
@RunWith(SpringRunner.class)
@SpringBootTest
// Override the scheduling rate to something really short:
@TestPropertySource(properties = "task.work.rate=100")
public class DemoApplicationTests {
@SpyBean
private MyTask myTask;
@Test
public void jobRuns() {
Awaitility.await().atMost(10, TimeUnit.SECONDS).untilAsserted(() ->
verify(myTask, Mockito.atLeastOnce()).work()
);
}
}
这通常很困难。您可以考虑在测试期间加载Spring上下文,并伪造其中的一些bean,以便能够验证计划的调用。
我在我的Github存储库中有这样的示例。这是一个简单的计划示例,使用了上述方法进行测试。
这个类是使用Spring Framework调度生成cron表达式的。
import org.apache.log4j.Logger;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.scheduling.support.CronSequenceGenerator;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
@RunWith(SpringJUnit4ClassRunner.class)
@Configuration
@PropertySource("classpath:application.properties")
public class TrimestralReportSenderJobTest extends AbstractJUnit4SpringContextTests {
protected Logger LOG = Logger.getLogger(getClass());
private static final String DATE_CURRENT_2018_01_01 = "2018-01-01";
private static final String SCHEDULER_TWO_MIN_PERIOD = "2 0/2 * * * *";
private static final String SCHEDULER_QUARTER_SEASON_PERIOD = "0 0 20 1-7 1,4,7,10 FRI";
@Test
public void cronSchedulerGenerator_0() {
cronSchedulerGenerator(SCHEDULER_QUARTER_SEASON_PERIOD, 100);
}
@Test
public void cronSchedulerGenerator_1() {
cronSchedulerGenerator(SCHEDULER_TWO_MIN_PERIOD, 200);
}
public void cronSchedulerGenerator(String paramScheduler, int index) {
CronSequenceGenerator cronGen = new CronSequenceGenerator(paramScheduler);
java.util.Date date = java.sql.Date.valueOf(DATE_CURRENT_2018_01_01);
for (int i = 0; i < index; i++) {
date = cronGen.next(date);
LOG.info(new java.text.SimpleDateFormat("EEE, MMM d, yyyy 'at' hh:mm:ss a").format(date));
}
}
}
以下是输出日志:
<com.medici.scheduler.jobs.TrimestralReportSenderJobTest> - lun, gen 1, 2018 at 12:02:02 AM
<com.medici.scheduler.jobs.TrimestralReportSenderJobTest> - lun, gen 1, 2018 at 03:02:02 AM
<com.medici.scheduler.jobs.TrimestralReportSenderJobTest> - lun, gen 1, 2018 at 06:02:02 AM
<com.medici.scheduler.jobs.TrimestralReportSenderJobTest> - lun, gen 1, 2018 at 09:02:02 AM
<com.medici.scheduler.jobs.TrimestralReportSenderJobTest> - lun, gen 1, 2018 at 12:02:02 PM
<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>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
</dependency>
count
,并在work
方法内将其递增: public class MyTask {
private final AtomicInteger count = new AtomicInteger(0);
@Scheduled(fixedRate=1000)
public void work(){
this.count.incrementAndGet();
}
public int getInvocationCount() {
return this.count.get();
}
}
count
:@SpringJUnitConfig(ScheduledConfig.class)
public class ScheduledIntegrationTest {
@Autowired
MyTask task;
@Test
public void givenSleepBy100ms_whenWork_thenInvocationCountIsGreaterThanZero()
throws InterruptedException {
Thread.sleep(2000L);
assertThat(task.getInvocationCount()).isGreaterThan(0);
}
}
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<version>3.1.6</version>
<scope>test</scope>
</dependency>
work
的调用次数:@SpringJUnitConfig(ScheduledConfig.class)
public class ScheduledAwaitilityIntegrationTest {
@SpyBean
MyTask task;
@Test
public void whenWaitOneSecond_thenWorkIsCalledAtLeastThreeTimes() {
await()
.atMost(Duration.FIVE_SECONDS)
.untilAsserted(() -> verify(task, atLeast(3)).work());
}
}
CronSequenceGenerator
类(链接):@Test
public void at50Seconds() {
assertThat(new CronSequenceGenerator("*/15 * 1-4 * * *").next(new Date(2012, 6, 1, 9, 53, 50))).isEqualTo(new Date(2012, 6, 2, 1, 0));
}
你可以在官方代码库中找到更多的例子。
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<version>${awaitility.version}</version>
<scope>test</scope>
</dependency>
package com.myco.tasks;
public class MyTask {
@Scheduled(fixedRate = ${app.scheduler.rate})
public void work() {
// task execution logic
}
}
@SpringBootTest
public class SchedulerTest {
@SpyBean
private MyTask myTask;
@Test
public void jobRuns() {
Awaitility.await()
.atMost(Durations.FIVE_SECONDS)
.untilAsserted(() -> Mockito.verify(myTask, Mockito.atLeast(1)).work());
}
}
Mockito
在这里对bean进行监视,Awaitility
进行轮询并设置超时时间。你可以在这里使用简单的 Thread.sleep()
,但是测试会变得更慢且不稳定,你要么需要等待更长时间,要么慢速可能会导致测试意外失败。
重要提示:你必须使用可配置的值来设置调度表达式,如果你的真实配置以小时或天为单位,那么测试配置应该以秒为单位。
优点:
缺点:
选项2. 任务调度持有者
package com.myco.tasks;
public class MyTask {
@Scheduled(cron = "${app.scheduler.cron}")
public void work() {
// task execution logic
}
}
@SpringBootTest
public class SchedulerTest {
@Value("${app.scheduler.cron}")
private String expectedCronExpression;
@Autowired
private ScheduledTaskHolder taskHolder;
@Test
public void cronTaskIsScheduled() {
CronTask cronTask = taskHolder.getScheduledTasks()
.stream()
.map(ScheduledTask::getTask)
.filter(CronTask.class::isInstance)
.map(CronTask.class::cast)
.findFirst()
.orElseThrow(() -> new IllegalStateException("No scheduled tasks"));
assertEquals(cronExpression, cronTask.getExpression());
assertTrue(cronTask.toString().contains("tasks.MyTask.work"));
}
}
CronTask
的,但可以很容易地适应OP示例,只需使用FixedRateTask
即可。ScheduledTaskHolder
,如果您使用的是Spring 3或更高版本,则可以直接注入ScheduledAnnotationBeanPostProcessor
。
@EnableScheduling
注解,就会有一个失败的测试。 - C-Otto