Java Developer? Przejdź na wyższy poziom wiedzy 🔥💪  Sprawdź

Team Leader? Podnieś efektywność swojego zespołu 👌 Sprawdź

7 sposobów na przyspieszenie Twoich testów w Spring

utworzone przez Java, Testowanie, Tip and Tricks

Co powoduje, że testy w Spring są wolne? Szybkość wykonywania testów ma kluczowy wpływ na to, jak szybko dostajemy feedback o poprawności wprowadzanej zmiany oraz jak szybko budowany jest projekt. To z kolei może spowolnić testowanie, a nawet wdrażanie w trybie Continuous Deployment.

  • W tym artykule omówię ogólne podejście do testowania.
  • Podzielę się też najczęstszymi błędami, z którymi spotkałem się w projektach powodujące wolne działanie testów.
  • Dowiesz się też, co powoduje, że testy w Spring stają się wolne i jak poprawić sytuację w projekcie.

Do artykułu powstało repozytorium z przykładami, dostępne na github: https://github.com/softwareskill/demo-testing-performance

1. Ustal strategię testowania – piramida testów

Już na początku projektu należy ustalić strategię testowania:

  • Ustalić piramidę testów – co będziemy testować testami end-to-end (aplikacyjnymi), komponentowymi.
  • Czym jest dla nas „Unit” w unit testach – skonfigurowany pakiet w izolacji od innych pakietów, klasa, albo mały kontekst Spring danego modułu w aplikacji.
  • W jaki sposób testujemy integracje
    • czy są to testy kontraktowe (Contract Tests, Consumer-Driven Contract)
    • czy uruchamiamy osobny zestaw testów integracyjnych, na jakim środowisku, czy potrzebujemy dostatecznej izolacji i wspomagamy się Test Containers.

Im więcej testów na wyższej warstwie piramidy testów tym są one wolniejsze. Dlatego najwięcej przypadków testowych należy testować na niższych warstwach, a całość działania aplikacji na wyższych.

Brzmi jak oczywistość, ale najwięcej sytuacji, z którymi się spotkałem, to nadmiarowa ilość testów na wyższych warstwach, które zależą od I/O (np. wysyłają requesty). Należy je zrefaktoryzować do niższych warstw. Na przykład testy aplikacyjne (end-to-end) spróbować zastąpić testami komponentowymi, a na wyższej warstwie zostawić kilka scenariuszy pozytywnych i negatywnych.

Inną heurystyką jest wydzielenie elementów, które zmieniają się często od tych, które zmieniają się rzadko. Na przykład, jeżeli w projekcie mamy sytuację, gdzie istnieje niestandardowa integracja (i nie można zastosować testów kontaktowych), ale rzadko się zmienia, ale mamy logikę biznesową, która zmienia się częściej – wolne testy integracyjne możemy wydzielić do osobnego pakietu testów i uruchamiać je na żądanie, tylko wtedy, gdy integracja się zmienia.

Piguła wiedzy o najlepszych praktykach testowania w Java

Pobierz za darmo książkę 100 stron o technikach testowania w Java

2. Zoptymalizuj testy integracyjne w Spring Boot

W Spring Boot testami integracyjnymi nazywane są testy, w których złożony jest kontekst Spring’a. Są świetne, dają wysoki poziom pewności, że wszystko jest skonfigurowane poprawnie, bez konieczności zbędnego mockowania.

Na początku działają szybko, ale wystarczy włączyć kilka modułów (Security, Data, Messaging) i kontekst Spring zaczyna działać wolno.

  • Nie należy testować działania samego frameworka.
  • Należy jednak przetestować, czy skonfigurowana aplikacja działa poprawnie.

Mógłby przecież pojawić się błąd, w którym oczekujemy zabezpieczonego endpoint’u, a dane są publicznie dostępne dla anonimowego użytkownika.

Testy integracyjne Spring mają swój koszt:

  • Są wolniejsze od zwykłych testów.
  • Pewnie włączają większy zakres funkcjonalności, który dla większości przypadków nie jest potrzebny.

Nie trzeba testować pełnej funkcjonalności aplikacji w pełnie skonfigurowanym kontekście Spring (z modułami Messaging, Web, Data). Można stworzyć znacznie mniejsze konteksty dla testów komponentowych.

Tetsy komponentowe jako testy integracyjne Spring

Innym rodzajem testów integracyjnych są testy komponentowe, które konfigurują wybrane pakiety klas lub całe moduły aplikacji. Całą funkcjonalność skonfigurować klasą @Configuration, włączyć ją w zestawie testów, a jednocześnie załączyć w kodzie produkcyjnym. Mamy pewność, że klasy są tak samo skonfigurowane w testach, jak i na produkcji.

W takim kontekście:

  • Izolujemy I/O (wejście, zapis do baz danych, eventów itd.)
  • Definiujemy fasadę pakietu/modułu (np. kilka publicznych serwisów) zamiast testowania endpointów http czy kolejkowych.

To znacznie ogranicza funkcjonalności integracyjne, które nawet w implementacjach in-memory, skutecznie spowolnią testy.

Tutaj warto skupić się na scenariuszach biznesowych. Każda zmiana w organizacji kodu wewnątrz pozwoli na zweryfikowanie, czy funkcje biznesowe nadal spełniają warunki.

3. Używaj Profili z rozwagą

Profile w Spring pozwalają włączyć zestaw parametrów (properties) oraz włączać funkcjonalności same w sobie. To doskonały mechanizm pozwalający na swobodną konfigurację trybów działania aplikacji, w tym Feature Toggles. W teście wystarczy użyć adnotacji @ActiveProfile.

Należy pamiętać, że za każdym razem, gdy używamy osobnego profilu, tworzony jest nowy ApplicationContext.

Dla małych kontekstów nie jest to problemem. Wystarczy jednak dodać kilka modułów i za każdym razem, gdy taki kontekst musi powstać, mija czas na stworzenie osobnych instancji.

Kilka wskazówek:

  • Jeżeli potrzebujesz profili – stwórz zbiorcze profile, np. test. Używaj go wtedy wszędzie.
    Alternatywą jest włączenie zastępczego zestawu klas @Configuration (występujących tylko w pakiecie testowym, nie produkcyjnym) izolujących I/O.
  • Staraj się unikać testowania profili w różnych kombinacjach. Pewnie da się to zrobić na niższym poziomie.

Zobacz przykład, w jaki sposób adnotacja @ActiveProfiles wpływa na sposób wykonywania się testów (uruchom pakiet *.suites.profiles).

Istnieją 2 testy:

@SpringBootTest
public class TestDefaultProfile {
 // ...
}
@SpringBootTest
@ActiveProfiles("profile-pln")
public class TestPlnProfile {
  // ...
}

Zauważ, że podczas wykonywania testów, profil Spring’a został zbudowany dwa razy od zera:

Psst… Interesujący artykuł?

Jeżeli podoba Ci się ten artykuł i chcesz takich więcej – dołącz do newslettera. Nie ominą Cię materiały tego typu.

.

4. Uważaj na @MockBean

@MockBean/@SpyBean to przydatne narzędzie wszędzie tam, gdzie na cele testów chcemy wyizolować zachowanie pewnego beana w kontekście. Ma to jednak swoje konsekwencje.

Za każdym razem, kiedy @MockBean występuje w teście, cache ApplicationContext jest oznaczany jako dirty, dlatego test runner wyczyści cache po zakończeniu testów w klasie testowej.

Zastanów się, dlaczego izolujesz pewną klasę. Może warto wykonać cały kod w kontekście springa, zamiast wycinać dane zależności?

Jeżeli kontekst testów integracyjnych jest duży, np. jest skonfigurowana cała aplikacja – zastosuj implementację in-memory baz danych lub warstw komunikacyjnych, jeżeli chcesz sprawdzić ich działanie. Nie musisz ich pisać, istnieje wiele dostępnych, np. H2 dla bazy danych, ActiveMQ, Embedded Kafka, itd.

Jak przyspieszyć działanie @MockBean

Jeżeli testujesz aplikację na poziomie component testów – odetnij zależności modułu dla wszystkich testów, nie tylko dla wybranych. To spowoduje, że w kontekście Spring będzie tylko jeden mock bean, dzięki czemu pomiędzy testami nie będzie czyszczony cache application context.

Spróbujmy zrefaktoryzować następujące testy:

@SpringBootTest
public class TestWithMock {

    @MockBean
    UserRepository userRepository;

    @Autowired
    UserSettingsService userSettingsService;

    @Test
    void getsDefaultCurrencyIfNotSet() {
        var user = new User();
        given(userRepository.findById(user.getId())).willReturn(Optional.of(user));

        var ccy = userSettingsService.getSelectedCurrency(user.getId());

        assertThat(ccy)
                .isEqualTo("USD");
    }
}
@SpringBootTest
public class TestWithMock2 {

    @Autowired
    UserSteps userSteps;

    @Autowired
    UserSettingsService userSettingsService;

    @Test
    void getsDefaultCurrencyIfNotSet() {
        var user = userSteps.givenUser();

        var ccy = userSettingsService.getSelectedCurrency(user.getId());

        assertThat(ccy)
                .isEqualTo("USD");
    }
}

Zauważ, że test TestWithMock wykorzystuje technikę @MockBean do zaślepienia repozytorium, a inny korzysta z UserSteps, który wykorzystuje implementację in-memory.

Zobacz przykład, w jaki sposób adnotacja @MockBean wpływa na sposób wykonywania się testów (uruchom pakiet *.suites.mock).

Zauważ, że profil springowy został przeładowany, raz wczytując implementację User’s repository, a raz nie (bo jest mock).

Zacznijmy optymalizację. Możemy albo ujednolicić testy do implementacji in-memory, lub jeżeli tego nie chcemy, zdefiniować jednego bean’a @MockBean:

@Configuration
public class MockConfig {

    @MockBean
    UserRepository userRepository;
}

I oba testy mogą swobodnie korzystać z mock’a UserRepository:

@SpringBootTest
public class TestWithMock {

    @Autowired
    UserRepository userRepository;

    @Autowired
    UserSettingsService userSettingsService;

    @Test
    void getsDefaultCurrencyIfNotSet() {
        var user = new User();
        given(userRepository.findById(user.getId())).willReturn(Optional.of(user));

        var ccy = userSettingsService.getSelectedCurrency(user.getId());

        assertThat(ccy)
                .isEqualTo("USD");
    }
}

* kod mockowania można uwspólnić, pokazuję tu jedynie technikę ponownego wykorzystania jednej instancji mock bean.

Uruchom teraz pakiet *.suites.mocksafe:

Profil został uruchomiony tylko jeden raz. Wszystkie testy korzystają z tego samego mock’a. Jest on automatycznie resetowany pomiędzy testami. Konfiguracja kontekstu Spring nie różni się pomiędzy testami.

5. Zarządzaj stanem zamiast używać @DirtiesContext

Wynik testu zawsze powinien być spójny, niezależnie od tego, czy jest wykonywany w izolacji, czy razem z innymi testami.

Zdarzają się sytuacje, że podczas testów z kontekstem Spring’a modyfikowany jest stan jakiegoś beana, który ma wpływ na inne testy. Przykładem może być:

  • Umieszczenie danych do różnego rodzaju baz / repozytoriów. Skutek jest taki, że są widoczne w innym teście.
  • Modyfikacja beana, który jest state-full (jest stanowy), np. zliczającego jakieś statystyki.

Efekt jest taki, że został zmodyfikowany stan po wykonaniu testu i chcemy go zresetować do wykonania kolejnego.

Jednym z rozwiązań jest użycie adnotacji @DirtiesContext.

W konsekwencji cache ApplicationContext jest oznaczany jako dirty i Spring zbuduje go od początku. To bardzo kosztowna operacja.

Zobacz przykład, w jaki sposób adnotacja @DirtiesContext wpływa na sposób wykonywania się testów (uruchom pakiet *.suites.dirtycontext). Zawiera on test o następującej konfiguracji:

@SpringBootTest
@DirtiesContext(classMode = AFTER_EACH_TEST_METHOD)
public class TestDirtiesContext {

Nie ma tu znaczenia jaki jest classMode, efekt będzie podobny w zależności od tego, ile klas testowych jest wykonywanych.

Istnieje kilka rozwiązań, aby uniknąć konieczności używania adnotacji @DirtiesContext.

  1. Po pierwsze zrozum swój kod. Co konkretnie się dzieje, że nie potrafisz uzyskać zamierzonego stanu? Czy nie jest tak, że używasz adnotacji @DirtiesContext „na ślepo”?
  2. Zauważ, że wiele mechanizmów już resetuje swój stan, na przykład @MockBean/@SpyBean, stany mocków są aytomatycznie resetowane przez Spring runner.
  3. Spróbuj usunąć / zresetować dane programistycznie (np. w sekcji @AfterEach), wystawiając metody reset / clean do swoich beanów. Jest to znacznie szybsze w wykonaniu. Zaburza to nieco API klasy, natomiast może być to tańsze w wykonaniu.

Psst… Interesujący artykuł?

Jeżeli podoba Ci się ten artykuł i chcesz takich więcej – dołącz do newslettera. Nie ominą Cię materiały tego typu.

.

6. Wykorzystaj Test slices

Test Slices to sposób segmentowania kontekstu Spring’a wprowadzony w Spring Boot 1.4. Pozwala on instruować Spring’a, które komponenty załadować na potrzeby naszych testów. Dla przykładu, gdy testujesz warstwę Web za pomocą MockMvc, być może niekoniecznie chcesz testować warstwę danych, a chcesz sprawdzić jedynie, jak odpowiada kontroler na różne sytuacje.

Przykładowym test-slice jest @WebMvcTest, który oznacza, że w kontekście springa musi być zapewnione środowisko web, i tylko dla danego kontrolera. Spring może podjąć pewne optymalizacje konfigurując minimum.

Inne test slices:

  • @DataJpaTest
  • @JdbcTest
  • @DataMongoTest
  • inne.

Możesz tworzyć również swoje własne test slices poprzez stworzenie adnotacji i załadowanie tylko potrzebnych konfiguracji.

7. Zrefaktoryzuj testy do niższego poziomu

Jeżeli mimo powyższych optymalizacji jakieś testy zajmują dużo czasu, sprawdź, czy scenariusze w nich zawarte muszą być wykonane na danym poziomie.

Za każdym razem, gdy wykonujemy testy integracyjne pokrywające kilka biznesowych przypadków testowych, to miejsce, gdzie należy się zastanowić, czy nie obniżyć poziomu testu.

Podsumowanie

W tym artykule opisałem przykłady testów integracyjnych oraz wartość jakie wnoszą. Ich budowa lub skomplikowanie może negatywnie wpłynąć na czas wykonywania. Istnieją techniki, aby obniżyć czas wykonywania testów. Należy cały czas przyglądać się czasowi wykonywania zestawu testowego, który jest krótki zwłaszcza na początku, ale może znacznie się wydłużać, gdy nie zadbamy o higienę.

Podoba Ci się ten artykuł? Weź więcej.

Jeżeli uważasz ten materiał za wartościowy i chcesz więcej treści tego typu – nie przegap ich i otrzymuj je prosto na swoją skrzynkę. Nawiążmy kontakt.

.
Do czego służy @DirtiesContext w Spring

Adnotacja @DirtiesContext oznacza kontekst Spring’a jako dirty po wykonaniu testu, np. gdy zmieniany jest stan aplikacji (beana). Po zmianie globalnego stanu testy mogą być niestabilne. Spring odbuduje kontekst Spring’a przed/po wykonaniu testu lub całej klasy. Użycie adnotacji jest kosztowne czasowo, istnieją inne techniki poradzenia sobie z problemem, o których przeczytasz tutaj.

Do czego służy @MockBean i @SpyBean w Spring

Adnotacja @MockBean i @SpyBean zastępuje beana w kontekście Spring’a mockiem, aby móc zaślepić lub obserwować zachowanie instancji. Niestety wprowadza to koszt czasowy, ponieważ kontekst Spring’a jest oznaczany jako dirty i musi zostać odbudowany. Dowiedz się, jak zoptymalizować działanie @MockBean i @SpyBean w Spring.

Do czego służy @ActiveProfiles w Spring

Adnotacja @ActiveProfiles w Spring buduje kontekst testowy z aktywnymi profilami. Dzięki temu można włączać i wyłączać poszczególne konfiguracje. Za każdym razem, gdy użyty jest @ActiveProfiles, kontekst jest budowany od nowa, co ma wpływ na czas wykonania testu. Staraj się unikać wielu kombinacji profili i pogrupuj testy o tym samym profilu.

Dyskusja