How to test Spring @Scheduled

50,523

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
Share:
50,523
Admin
Author by

Admin

Updated on December 04, 2020

Comments

  • Admin
    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
    rohit over 6 years
    Just 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
    luboskrnac over 6 years
    @rohit, Feel free to post your solution. If you don't, I assume you don't have one.
  • LiTTle
    LiTTle over 5 years
    verify() and times() functions cannot be found. Could you specify the package?
  • Maciej Walkowiak
    Maciej Walkowiak over 5 years
    These functions come from Mockito. The package is: org.mockito.Mockito#verify and similar for times.
  • Cristian Batista
    Cristian Batista about 5 years
    This is not a good solution. This only works for those @Scheduled that are executed in some seconds. What about a weekly execution?
  • Maciej Walkowiak
    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ò
    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
    DependencyHell almost 3 years
    CronSequenceGenerator 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
    Code Name Jack about 2 years
    Yeah the second one, because it is easy to miss the configuration (or not know it properly).