39

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
     }
 }
DwB
  • 35,321
  • 10
  • 55
  • 81
S Puddin
  • 391
  • 1
  • 3
  • 5
  • 9
    What do you want to test exactly? If you want to test that work() does what it's supposed to do, you can test it like any other method of any other bean: you create an instance of the bean, call the method, and test that it does what it's supposed to do. If you want to test that the method is indeed invoked by Spring every second, there's no real point: Spring has tested that for you. – JB Nizet Aug 31 '15 at 21:00
  • I agree with you, trying to test the framework's functionality did not seem necessary to me but I was required to. I found a work around for that by adding a small log message and checking if the expected message was indeed logged for the expected time frame. – S Puddin Sep 16 '15 at 14:34
  • 4
    Another benefit of testing is to have a failing test if the `@EnableScheduling` annotation is removed. – C-Otto Nov 09 '17 at 13:30

6 Answers6

36

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());
    }
}
Maciej Walkowiak
  • 11,422
  • 56
  • 59
  • 1
    `verify()` and `times()` functions cannot be found. Could you specify the package? – LiTTle Aug 06 '18 at 06:40
  • 1
    These functions come from Mockito. The package is: `org.mockito.Mockito#verify` and similar for `times`. – Maciej Walkowiak Aug 06 '18 at 12:17
  • 1
    This is not a good solution. This only works for those @Scheduled that are executed in some seconds. What about a weekly execution? – Cristian Batista Jan 30 '19 at 16:56
  • 1
    @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 :-) – Maciej Walkowiak Jan 30 '19 at 18:19
  • 2
    @CristianBatista you can use a different frequency for the cron job in testing, by using a property instead of hardcode it. – Niccolò Jan 07 '20 at 13:00
27

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.
  }
DwB
  • 35,321
  • 10
  • 55
  • 81
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.

luboskrnac
  • 22,667
  • 9
  • 77
  • 88
  • 4
    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. – rohit Sep 23 '17 at 13:57
  • 3
    @rohit, Feel free to post your solution. If you don't, I assume you don't have one. – luboskrnac Sep 25 '17 at 06:17
2

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
Tiago Medici
  • 1,514
  • 18
  • 17
  • 1
    CronSequenceGenerator is now Deprecated as of 5.3, in favor of CronExpression, check org.springframework.scheduling.support.CronTrigger usage in this example : https://stackoverflow.com/a/33504624/2641426 – DependencyHell May 06 '21 at 16:09
2

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()
        );
    }
}
Mifeet
  • 12,006
  • 4
  • 55
  • 100
1

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.

JuanMoreno
  • 1,991
  • 1
  • 20
  • 30