47

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
  • 37,124
  • 11
  • 56
  • 82
S Puddin
  • 471
  • 1
  • 4
  • 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
  • 5
    Another benefit of testing is to have a failing test if the `@EnableScheduling` annotation is removed. – C-Otto Nov 09 '17 at 13:30

7 Answers7

43

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
  • 12,372
  • 59
  • 63
  • 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
  • 3
    @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
32

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
  • 37,124
  • 11
  • 56
  • 82
5

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,949
  • 5
  • 60
  • 108
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
  • 23,973
  • 10
  • 81
  • 92
  • 6
    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,944
  • 22
  • 22
  • 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

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
  • 2,498
  • 1
  • 25
  • 34
0

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:

  • Simple to write
  • Tests actual bean calls

Disadvantages:

  • Doesn't test actual expression, test configuration might be different from real configuration in case of scheduling big intervals.
  • Using very short interval might cause disruption in other Spring tests, there's a probability that scheduler might kick-in. Can be avoided by using a dedicated profile for this test.

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:

  • Can test the actual exact cron expression

Disadvantages:

  • Doesn't test bean calls
  • Harder to refactor: class names, method names (and package, if you include that in your assertion) change should be adjusted manually.

You can combine both approaches if you're testing a very critical scheduler. I'd recommend using different profiles for tests.

J-Alex
  • 6,881
  • 10
  • 46
  • 64