How do I test @Scheduled
job tasks in my spring-boot application?
package com.myco.tasks;
public class MyTask {
@Scheduled(fixedRate=1000)
public void work() {
// task execution logic
}
}
How do I test @Scheduled
job tasks in my spring-boot application?
package com.myco.tasks;
public class MyTask {
@Scheduled(fixedRate=1000)
public void work() {
// task execution logic
}
}
If we assume that your job runs in such a small intervals that you really want your test to wait for job to be executed and you just want to test if job is invoked you can use following solution:
Add Awaitility to classpath:
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<version>3.1.0</version>
<scope>test</scope>
</dependency>
Write test similar to:
@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());
}
}
My question is: "what do you want to test?"
If your answer is "I want to know that Spring runs my scheduled task when I want it to", then you are testing Spring, not your code. This is not something you need to unit test.
If your answer is "I want to know that I configured my task correctly", then write a test app with a frequently running task and verify that the task runs when you expect it to run. This is not a unit test, but will show that you know how to configure your task correctly.
If the answer is "I want to know that the task I wrote functions correctly",
then you need to unit test the task method.
In your example,
you want to unit test the work()
method.
Do this by writing a unit test that directly calls your task method (work()
).
For example,
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.
}
Answer from @Maciej solves the problem, but doesn't tackle the hard part of testing @Scheduled with too long intervals (e.g. hours) as mentioned by @cristian-batista .
In order to test @Scheduled independently of the actual scheduling interval, we need to make it parametrizable from tests. Fortunately, Spring has added a fixedRateString
parameter for this purpose.
Here's a complete example:
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
}
}
Test with 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()
);
}
}
This is often hard. You may consider to load Spring context during the test and fake some bean from it to be able to verify scheduled invocation.
I have such example in my Github repo. There is simple scheduled example tested with described approach.
this class stands for generating schedulers cron using springframework scheduling
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));
}
}
}
here is the output logging:
<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
We can use at least two approaches in order to test scheduled tasks with Spring:
If we use spring boot we gonna need the following dependencies:
<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>
We could add a count
to the Task
and increment it inside the work
method:
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();
}
}
Then check the 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);
}
}
In that case, we need to add the Awaitility dependency:
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<version>3.1.6</version>
<scope>test</scope>
</dependency>
And use its DSL to check the number of invocations of the method 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());
}
}
We need take in count that although they are good it’s better to focus on the unit testing of the logic inside the work method.
I put an example here.
Also, if you need to test the CRON expressions like "*/15 * 1-4 * * *" you can use the CronSequenceGenerator
class:
@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));
}
You can find more examples in the official repository.
First, great answers and comments were posted here, but I'd like to add some summary and one more additional approach you can use.
1. Why you might want to test that?
It is true that there's no reason to test Spring's mechanisms. However, there's another side to it. In order for the entire scheduling mechanism to work, you need @EnableScheduling
annotation on configuration level or and @Scheduled
annotation + a cron expression. There are many places where it can go wrong. Imagine someone accidentally removes one of the annotations, or puts @Scheduled
to a wrong method. Cron expression is verified by Spring from a technical point of view, but not from a business angle.
2. How you can test that?
We don't need this test to be complex and test business logic inside it, the business logic it tested in other tests. We will focus on testing the fact of working scheduler.
Option 1. Spy Bean and waiting mechanism
Add this dependency:
<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
spies on the bean here, Awaitility
makes the polling and sets the timeout. You can use simple Thread.sleep()
here, but the test will just be slower and flaky, you either need to wait longer, or slowness can just make it fail accidentally.
Important note: you have to use configurable values for the scheduling expressions, if your real config measured in hours or days, the test config should be measured in seconds.
Advantages:
Disadvantages:
Option 2. Task Scheduling holders
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"));
}
}
Inject the task holder and assert if there are scheduled tasks by asserting the expression and Class + method name. This example is given for CronTask
but can easily be adapted for OP example using FixedRateTask
.
Important note: ScheduledTaskHolder
is available since Spring 5.0.2, you can inject ScheduledAnnotationBeanPostProcessor
directly if you're using Spring 3 or above.
Advantages:
Disadvantages:
You can combine both approaches if you're testing a very critical scheduler. I'd recommend using different profiles for tests.