How to test Spring @Scheduled
Solution 1
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());
}
}
Solution 2
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.
}
Solution 3
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.
Solution 4
We can use at least two approaches in order to test scheduled tasks with Spring:
- Integration testing
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);
}
}
- Another alternative is using Awaitility like mentions @maciej-walkowiak.
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.
Solution 5
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
Admin
Updated on December 04, 2020Comments
-
Admin over 3 years
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 } }
-
rohit over 6 yearsJust waiting for the scheduled task is definitely not the way. Should be a trick to play with the clock so that scheduler can respond to it.
-
luboskrnac over 6 years@rohit, Feel free to post your solution. If you don't, I assume you don't have one.
-
LiTTle over 5 years
verify()
andtimes()
functions cannot be found. Could you specify the package? -
Maciej Walkowiak over 5 yearsThese functions come from Mockito. The package is:
org.mockito.Mockito#verify
and similar fortimes
. -
Cristian Batista about 5 yearsThis is not a good solution. This only works for those @Scheduled that are executed in some seconds. What about a weekly execution?
-
Maciej Walkowiak about 5 years@CristianBatista "If we assume that your job runs in such a small intervals". I don't think it makes much sense to test if job runs but rather the job behaviour. Nevertheless if you really do want to, that's one of the options I am aware of. You're welcome to submit your answer too :-)
-
Niccolò over 4 years@CristianBatista you can use a different frequency for the cron job in testing, by using a property instead of hardcode it.
-
DependencyHell almost 3 yearsCronSequenceGenerator is now Deprecated as of 5.3, in favor of CronExpression, check org.springframework.scheduling.support.CronTrigger usage in this example : stackoverflow.com/a/33504624/2641426
-
Code Name Jack about 2 yearsYeah the second one, because it is easy to miss the configuration (or not know it properly).