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.

Author: Dariusz Mydlarz

Cześć, nazywam się Dariusz Mydlarz i od 2012 pracuję jako programista. Tworzę w ekosystemie Javy i uwielbiam systemy backendowe. Chcę w tym miejscu pomagać Ci stawać się lepszym programista. Jeśli mogę Ci jakoś pomoc, po prostu napisz do mnie.

🚀 Trwają zapisy na webinar - Najlepszy sposób na naukę Springa - 26 stycznia o 21:00 (wtorek)Rezerwuję miejsce!
+