Jak przetestowa膰 adnotacj臋 @Scheduled w Springu? (+Video 馃摴)

Korzystasz z adnotacji @Scheduled w swoim Springowym projekcie i chcia艂by艣 mie膰 pewno艣膰, 偶e wykona si臋 ona poprawnie? Chcia艂by艣 napisa膰 do niej test, ale nie do ko艅ca wiesz jak si臋 za niego zabra膰? Zapraszam do 艣rodka, gdzie rozwiej臋 Twoje wszystkie w膮tpliwo艣ci! 馃檪

馃摴 Na samym dole znajdziesz wersj臋 wideo.

Za艂贸偶my, 偶e Twoja aplikacja wykonuje jakie艣 cykliczne zadania. Na przyk艂ad raz dziennie wysy艂a maile do u偶ytkownik贸w z informacjami o nowych ofertach. W tym celu skorzysta艂e艣 z adnotacji @Scheduled za pomoc膮, kt贸rej zdefiniowa艂e艣 cz臋stotliwo艣膰 wykonywania si臋 zadania.

@Component
@AllArgsConstructor
class MailJob {
    private final MailService mailService;

    @Scheduled(cron = "0 0 7 * * MON-FRI")
    public void run() {
        mailService.sendEmails();
    }

}

Wyra偶enie CRON 0 0 7 * * MON-FRI oznacza, 偶e metoda run() b臋dzie wykonywa膰 si臋 od poniedzia艂ku do pi膮tku o 7:00.

Jak napisa膰 test do takiej klasy? Rozwi膮zanie, w kt贸rym czekamy z testami codziennie do 7:00 nie brzmi jak najlepszy pomys艂 馃槈

Opcje s膮 dwie.

Przetestowanie klasy unitowo

Rozwi膮zanie pierwsze to przetestowanie zachowania z pomini臋ciem frameworka. W tym podej艣ciu zak艂adamy, 偶e adnotacja @Scheduled jest zaimplementowana we frameworku poprawnie i to co nas interesuje to zachowanie samej metody run().

Jak mo偶e wygl膮da膰 wtedy taki test?

@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {MailJob.class, MailService.class})
class MailJobTest {

    @Autowired
    MailService mailService;

    @Autowired
    MailJob mailJob;

    @Test
    public void shouldSendMails() {
        // when
        mailJob.run();

        // then
        assertEquals(1L, mailService.sentEmailsCount());
    }

}

Gdzie MailService dla potrzeb przyk艂adu to bardzo prosta klasa przygotowana na potrzeby demonstracji.

@Service
class MailService {
    private final LongAdder counter = new LongAdder();

    public void sendEmails() {
        System.out.println("Would send emails...");
        counter.increment();
    }

    public long sentEmailsCount() {
        return counter.sum();
    }
}

W tym wypadku nie testujemy tego, 偶e operacja wykona si臋 w dni robocze o 7:00 rano, ale tylko to co ona w艂a艣ciwie robi. Czyli, 偶e wo艂a metod膮 wysy艂aj膮c膮 maile z klasy MailService.

I jest to bardzo w艂a艣ciwe podej艣cie. Twoja logika metody run() mo偶e by膰 bardziej rozbudowana.

Sprawd藕 w te艣cie, 偶e w wyniku zawo艂ania tej metody w systemie zachodz膮 oczekiwane operacje i zmiany.

Testujemy zachowanie integracyjne

Mo偶liwe jest te偶 przetestowanie zachowania integracyjne.

Potrzebujemy wtedy wprowadzi膰 jedynie ma艂膮 zmian臋 do klasy MailJob.

Zamiast definiowa膰 wyra偶enie cron bezpo艣rednio w kodzie przenosimy je do propertiesa wstrzykiwanego przez Springa.

@Component
@AllArgsConstructor
class MailJob {
    private final MailService mailService;

    @Scheduled(cron = "${app.mail-job.cron}")
    public void run() {
        mailService.sendEmails();
    }

}

W pliku src/main/resources/application.properties wpisujemy oczekiwan膮 produkcyjn膮 warto艣膰 parametru.

app.mail-job.cron=0 0 7 * * MON-FRI

Natomiast test w贸wczas przyjmuje nast臋puj膮ca posta膰.

@SpringBootTest(properties = {
    "app.mail-job.cron=*/1 * * * * *"
})
class MailJobIT {

    @Autowired
    MailService mailService;

    @Test
    public void shouldSendMails() {
        Awaitility.await()
                  .atMost(5, TimeUnit.SECONDS)
                  .until(() -> mailService.sentEmailsCount() > 0);
    }

}

Za pomoc膮 adnotacji @SpringBootTest uruchomi艂em ca艂y kontekst Springa dla cel贸w tego testu, oraz nadpisa艂em warto艣膰 parametru, kt贸ry jest wstrzykiwany do klasy MailJob.

@SpringBootTest(properties = {
    "app.mail-job.cron=*/1 * * * * *"
})

Metoda run() b臋dzie teraz wykonywa膰 si臋 co sekund臋 – zgodnie z nadpisanym wyra偶eniem cron */1 * * * * *.

Po drugie skorzysta艂em z biblioteki Awaitility, za pomoc膮 kt贸rej mog臋 w elegancki spos贸b napisa膰 test oczekuj膮cy na jaki艣 warunek.

Awaitility.await()
            .atMost(5, TimeUnit.SECONDS)
      .until(() -> mailService.sentEmailsCount() > 0);

Powy偶szy fragment oznacza, 偶e przez maksymalnie 5 sekund oczekujemy spe艂nienia warunku:

mailService.sentEmailsCount() > 0

Je艣li nasze zadanie oznaczone adnotacj膮 @Scheduled wykona si臋 poprawnie, to test za艣wieci si臋 na zielono. Czego w艂a艣nie oczekujemy聽馃檪

Alternatywna droga

Jest jeszcze jedna, alternatywna droga.

Zamiast testowa膰 zachowanie i dzia艂anie frameworka mo偶emy doda膰 do projektu metryki i monitoring.

Dzi臋ki temu mo偶emy mierzy膰 – jak w podanym przyk艂adzie – ile maili zosta艂o wys艂anych dziennie z systemu i za pomoc膮 monitoringu walidowa膰, 偶e operacje faktycznie wykonuj膮 si臋 zgodnie z naszymi oczekiwaniami.

W przeciwnym wypadku podnosi膰 alerty, kt贸re poinformuj膮 zesp贸艂 deweloperski o problemach w projekcie.

Ale to ju偶 opowie艣膰 na inny wpis.

PS. Je艣li ten temat (metryk i monitoringu) by Ci臋 interesowa艂, daj zna膰 koniecznie w komentarzu 馃檪

Kod 藕r贸d艂owy

Kod 藕r贸d艂owy powi膮zany z tym wpisem znajdziesz w repozytorium na Githubie

Artyku艂 dost臋pny tak偶e w wersji wideo

Wolisz ogl膮da膰? Przygotowa艂em dla Ciebie r贸wnie偶 wersj臋 wideo.