Testing @Scheduled annotations with Spring (Boot)

Featured image for sharing metadata for article

If you're writing a Spring (Boot) application that performs actions periodically, it's likely that you may be using the @Scheduled annotation.

Unfortunately, there's no test slice or mocking/stubbing that we can do to make it possible to test these out-of-the-box, and instead need to execute it for real.

Let's say that we want to test that a method is called once an hour:

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class ScheduleHandler {

  @Scheduled(fixedRate = 3_600_000)
  public void onSchedule() {
    // do something
  }
}

I hope I don't surprise you, dear reader, by saying I don't want to have a test running for an hour.

So what can we do? Well, similar to the way that we make it easier to test individual components in our codebase, we'd want to employ dependency injection.

We can instead inject the schedule rate with a new property i.e. fetch-rate:

 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Component;

 @Component
 public class ScheduleHandler {

-  @Scheduled(fixedRate = 3_600_000)
+  @Scheduled(fixedRateString = "${fetch-rate:3600000}")
   public void onSchedule() {
     // do something
   }
 }

This then allows us to write the following test:

import static org.awaitility.Awaitility.await;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.verify;

import java.time.Duration;
import java.time.temporal.ChronoUnit;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.SpyBean;
import uk.gov.api.springboot.infrastructure.ScheduleHandler;

@SpringBootTest(properties = "fetch-rate=50")
class ApplicationTest {
  @SpyBean private ScheduleHandler scheduleHandler;

  @Test
  void scheduleIsTriggered() {
    await()
        .atMost(Duration.of(200, ChronoUnit.MILLIS))
        .untilAsserted(() -> verify(scheduleHandler, atLeast(1)).onSchedule());
  }
}

Notice that we're using awaitility for this as a handy DSL, but an alternative using Thread.sleeps would work, too.

Instead of placing the fetch-rate property in the @SpringBootTest annotation, we could also create the file src/test/resources/application-test.properties:

fetch-rate=50

This can also work with other expressions, such as the Spring cron-like:

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class ScheduleHandler {

  @Scheduled(cron = "${fetch-rate:0 * * * * MON-FRI}")
  public void onSchedule() {
    // do something
  }
}

Which can be tested like so:

import static org.awaitility.Awaitility.await;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.verify;

import java.time.Duration;
import java.time.temporal.ChronoUnit;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.SpyBean;
import uk.gov.api.springboot.infrastructure.ScheduleHandler;

@SpringBootTest(properties = "fetch-rate=* * * * * *")
class ApplicationTest {
  @SpyBean private ScheduleHandler scheduleHandler;

  @Test
  void scheduleIsTriggered() {
    await()
        .atMost(Duration.of(1500, ChronoUnit.MILLIS))
        .untilAsserted(() -> verify(scheduleHandler, atLeast(1)).onSchedule());
  }
}

Written by Jamie Tanna's profile image Jamie Tanna on , and last updated on .

Content for this article is shared under the terms of the Creative Commons Attribution Non Commercial Share Alike 4.0 International, and code is shared under the Apache License 2.0.

#blogumentation #spring #spring-boot #tdd.

Also on:

This post was filed under articles.

Interactions with this post

Interactions with this post

Below you can find the interactions that this page has had using WebMention.

Have you written a response to this post? Let me know the URL:

Do you not have a website set up with WebMention capabilities? You can use Comment Parade.