Jak poprawnie napisać testy integracyjne?

0

Hej, mam małe pytanka o testowanie integracyjne w Springu (pytania 1,2 w sumie ogólne):

  1. Czy testy integracyjne mogą współdzielić stan, czy po każdym powinienem czyściś/incijalizować DB ponownie? W sumie wydaje mi się, że szybciej byłoby raz wypełnić danymi niż przed każdym testem

  2. Jaki jest najlepszy sposób dla wypełnienia DB przed testami - użyć normalnie usługi REST i wysłać żądania (np. utworzyć tak max kilka obiektów) czy jakoś inaczej wypełnić DB (np. adnotacja @Sql - https://docs.spring.io/spring-framework/reference/testing/annotations/integration-spring/annotation-sql.html)

  3. Czy mam jakąś gwarancję, że kolejność wykonania moich testów będzie zawsze taka sama jeśli nie używam adnotacji @Order. Coś czytałem o jakiejś domyślnej kolejności, ale coś mi się nie zgadza jak patrzę na kolejność wykonania testów w Intellij (czy za pomocą przycisku w IDE, czy mvn clean install)

  4. Z tego co czytam, to domyślnie JUnit zakłada, że dla każdego testu instacja klasy jest tworzona ponownie. Ale jak rozumiem - nie dotyczy to automatycznego czyszczenia bazy (dla testów mam h2)? Po wykonaniu początkowych testów, które testują tworzenie - w kolejnych które testują odczyt np. listy obiektów widać te nowo utworzone w poprzednich testach

3

1, 3. Testy powinny być niezalezne od siebie więc nie powinny wspóldzielić stanu i nie powinny być wrażliwe na kolejność ich wykonywania.
2. Nie ma czegoś takiego jak najlepszy sposób. Wszystko zależy. Jeżeli masz na myśli wypełnienie db jakimiśencjami któe potem będziesz chciał odczytać np. na front to ja bym to wrzucał w konkretnych testach aby zachować niezależność, jeżeli chodzi o jakąś konfigurację któa w trakcie testów się nie zmienia to możesz to wrzucić wcześniej.
4. między testami powinieneś czyścić bazę więc jeżeli masz testy któe testują jakiś odczyt to powinienes najpierw nowe obiekty w db umieścić (czy zrobisz to ręcznie slq'em czy strzelając na endpoint to zależy od Ciebie)

Protip: zainteresuj siętestcontainers bo h2 w testach integracyjnych jednak nie oddaje 100% stanu rzeczywistego (silniki db różnią się do siebie).

1
  1. W Praktyce zawsze każdy test "stawia" nową bazę, to może być H2, ale może też byc testcontainers. Testy powinny być niezależne.
  2. To zależy co testujesz, zwykle testuje "logikę biznesową" a nie samą bazę. Ale jak potrzebujesz danych, to ja bym je wypełniał na poziomie @BeforeEach przy pomocy repo, albo jakiegoś helpera
2

Określenie "test integracyjny" stało się tak popularne że nie ma jednej definicji tego czym test integracyjny jest lub nie jest - spytasz 10 osób, dostaniesz 10 odpowiedzi. Podobnie jest z nazwami "test jednostkowy" albo "test e2e" - nie ma standardów czy kryteriów, i wielu programistów bardzo lekko się porusza tymi terminami, bez żadnych konkretnych granic między tym kiedy kończy się jeden, a gdzie zaczyna się drugi. Także posługiwanie się takimi nazwami jest raczej mało jednoznaczne.

cerrata napisał(a):
  1. Czy testy integracyjne mogą współdzielić stan, czy po każdym powinienem czyściś/incijalizować DB ponownie? W sumie wydaje mi się, że szybciej byłoby raz wypełnić danymi niż przed każdym testem

Powinno być tak, żebyś mógł odpalić dowolną ilość testów w dowolnej kolejności, i ich wynik powinien być taki sam nie ważne czy się odpala test w pojedynkę, czy przed/po innym. Jeśli jesteś w stanie to zapewnić pojedynczym seedem bazy to super. Jeśli nie, to musisz dołożyć staran żeby zapewnić niezależność testów od siebie.

  1. Jaki jest najlepszy sposób dla wypełnienia DB przed testami - użyć normalnie usługi REST i wysłać żądania (np. utworzyć tak max kilka obiektów) czy jakoś inaczej wypełnić DB (np. adnotacja @sql - https://docs.spring.io/spring-framework/reference/testing/annotations/integration-spring/annotation-sql.html)

Oba z nich są okej, musisz tylko rozważyć wady i zalety.

  1. Czy mam jakąś gwarancję, że kolejność wykonania moich testów będzie zawsze taka sama jeśli nie używam adnotacji @Order. Coś czytałem o jakiejś domyślnej kolejności, ale coś mi się nie zgadza jak patrzę na kolejność wykonania testów w Intellij (czy za pomocą przycisku w IDE, czy mvn clean install)

A czemu miałbyś na tym polegać? To jest bardzo dobre że runner odpala testy w losowej kolejności.

  1. Z tego co czytam, to domyślnie JUnit zakłada, że dla każdego testu instacja klasy jest tworzona ponownie. Ale jak rozumiem - nie dotyczy to automatycznego czyszczenia bazy (dla testów mam h2)? Po wykonaniu początkowych testów, które testują tworzenie - w kolejnych które testują odczyt np. listy obiektów widać te nowo utworzone w poprzednich testach

No nie, instancja testu to jedna rzecz, instancja bazy to inna.

0
cerrata napisał(a):

Hej, mam małe pytanka o testowanie integracyjne w Springu (pytania 1,2 w sumie ogólne):

  1. Czy testy integracyjne mogą współdzielić stan, czy po każdym powinienem czyściś/incijalizować DB ponownie? W sumie wydaje mi się, że szybciej byłoby raz wypełnić danymi niż przed każdym testem

Najlepiej, żeby współdzieliły, bo dzięki temu twój test testuje co się dzieje jak właśnie to się dzieje. Oczywiście nie zawsze jest to możliwe. Jeśli w twojej apce wszystko jest przypisane do użytkownika i potrafisz zrobić testy tak, że każdy test odnosi się do innego usera i testy nie walczą ze sobą to super.

  1. Jaki jest najlepszy sposób dla wypełnienia DB przed testami - użyć normalnie usługi REST i wysłać żądania (np. utworzyć tak max kilka obiektów) czy jakoś inaczej wypełnić DB (np. adnotacja @Sql - https://docs.spring.io/spring-framework/reference/testing/annotations/integration-spring/annotation-sql.html)

Najlepiej REST: testujesz kontrakt/powiązanie pomiędzy metodami. Do tego nie wprowadzisz stanu, który nie jest dozwolony przez wystawione usługi

0
slsy napisał(a):

Najlepiej REST: testujesz kontrakt/powiązanie pomiędzy metodami. Do tego nie wprowadzisz stanu, który nie jest dozwolony przez wystawione usługi

A skąd wniosek, że restem da się wsadzić dane które w teście integracyjnym chcesz odczytać?
Bzdura poza tym mylisz test interacyjny z testem end to end.

1
RequiredNickname napisał(a):
slsy napisał(a):

Najlepiej REST: testujesz kontrakt/powiązanie pomiędzy metodami. Do tego nie wprowadzisz stanu, który nie jest dozwolony przez wystawione usługi

A skąd wniosek, że restem da się wsadzić dane które w teście integracyjnym chcesz odczytać?

Jeśli nie da się ich wsadzić ani odczytać interfejsem aplikacji, to bardzo prawdopodobne że nie warto tego testować bo to szczegół implementacyjny.

Możesz dać przykład takiej rzeczy?

1

No i w przypadku testu integracyjnego powinno się jak najbardziej polegać na realnych funkcjonalnościach a nie mockach. Wtedy ma to sens.

0

@Riddle: tak się rozpisałeś wcześniej a imho mylisz test integracyjny z e2e.
Przecież test integracyjny w ogóle nie musi dotykać resta pod względem funkcjonalności bo np. aplikacja może sięgać do tabelki którą zasila ktoś inny/inna aplikacja a Ty chcesz przetestować wyciągnięcie w aplikacji tych danych i np. zmapować to na jakąś konfigurację whatever. Usecase'ów jest mnóstwo...

Z resztą po to spring dostarcza klocki aby podnosić context pod integracjęz db ale już niekoniecznie do komunikacji po http itp.

0
RequiredNickname napisał(a):

@Riddle: tak się rozpisałeś wcześniej a imho mylisz test integracyjny z e2e.

To daj swoje definicje tych terminów.

Przecież test integracyjny w ogóle nie musi dotykać resta pod względem funkcjonalności bo np. aplikacja może sięgać do tabelki którą zasila ktoś inny/inna aplikacja a Ty chcesz przetestować wyciągnięcie w aplikacji tych danych i np. zmapować to na jakąś konfigurację whatever. Usecase'ów jest mnóstwo...

Jeśli tabelkę zasila coś innego (np przez jakąś inną aplikację), to ta tabelka również jest interfejsem aplikacji i również powinna być przetestowana. Taki test, jak rozumiem, według Twojej definicji również powinien być e2e.

Chyba że jest zasilany przez coś co jest częścią aplikacji, w ten czy inny sposób, wtedy to jednak jest szczegół implementacyjny. Jeśli takie pole w tabelce jest zasilane np przez zadanie na kolejce, a zadanie na kolejce jest odpalane przez endpoint, to to też jest szczegół implementacyjny aplikacji, i dobry test powinien odpalić endpoint, który odpala kolejkę, która wsadza pole do bazy, i potem zrobić asercję na endpoincie polu z tabelki. Jeśli stwarza to problemy z performance'em, to wtedy można podmienić implementacje kolejek na takie które są in-memory, albo działają szybciej.

Z resztą po to spring dostarcza klocki aby podnosić context pod integracjęz db ale już niekoniecznie do komunikacji po http itp.

To że biblioteka tak robi, w żaden sposób nie oznacza tego że to jest dobry sposób, albo że tak się powinno robić. Biblioteki są pełne słabych rozwiązań, i nie jest to argument.

1
  • Nie ma czegoś takiego jak "testy integracyjne" - to termin wieloznaczny (jak wzmiankował @Riddle ) - w większości przypadków te testy pisze się dokładnie tak samo jak inne testy - testy to testy.
  • Junit ma wiele specyficznych ograniczeń - zasad, np. metody testowe muszą być niezależne, bo kolejność nie jest gwarantowana, jednak inne frameworki niekoniecznie takie ograniczenia mają. Np. Kotest ma / może mieć określoną kolejność. Kotest polecam nawet jak piszesz w javie, to testy w kotlinie i tak wyglądają i działają lepiej.
  • Testy/asercje powinny być niezależne, ale jeśli ze względów np. na czas odpalania testów musimy to nagiąć to w takim kotest można. W zasadzie można patrzeć na to jak jeden długi test/scenariusz, który składa się z wielu etapów z różnymi asercjami, (w junit można by zrobić jedną długą metodę testową, albo sprytnie użyć dynamic test)
  • Robimy testy niezależne, aby można je było odpalać np. równolegle, ale jeśli się okazuje, ze to powoduje więcej problemów niż zysku to można złamać (jak wyżej) o ile oczywiście korzystamy z frameworku, który na to pozwala.
  • Bazę można zasilać różnie, np. zrzut SQL, można też mieć pustą i zasilać tylko danymi na potrzeby konkretnego testu.
  • Testcontainers polecam, ale nie dogmatycznie, w większości projektów H2 robi wystarczającą robotę, a wstaje jednak szybciej. Z drugiej strony tescontainers daje tony dodatkowych możliwości - np. testowanie zrywania połączenia z bazą etc.
0
RequiredNickname napisał(a):

@Riddle: tak się rozpisałeś wcześniej a imho mylisz test integracyjny z e2e.

Nie ma definicji tych testów, nawet nie ma definicji unit testu. Kolektywna wiedza mówi, że test integracyjny to coś co testuje aplikację w odosobnieniu od innych aplikacji, czyli walimy po REST i dotykamy prawdziwej bazy.

E2E testuje cały produkt, czyli kilka aplikacji w odosobnieniu. Przy prostych projektach integracyjne to to samo co E2E

Google ma dobry podział na testy https://testing.googleblog.com/2010/12/test-sizes.html , gdzie IMO integracyjne to odpowiednik medium a E2E large

0
slsy napisał(a):

Google ma dobry podział na testy https://testing.googleblog.com/2010/12/test-sizes.html , gdzie IMO integracyjne to odpowiednik medium a E2E large

Temu podziałowi to raczej bym nie ufał w kontekście dobrych testów, bo tam mają limit sekund na małe 60 sekund, a na duże 900.

Nie wiem co oni tam biorą, ale to na pewno nie jest definicja dobrych testów.

0
Riddle napisał(a):

Jeśli tabelkę zasila coś innego (np przez jakąś inną aplikację), to ta tabelka również jest interfejsem aplikacji i również powinna być przetestowana. Taki test, jak rozumiem, według Twojej definicji również powinien być e2e.

Pytałeś o przykład i przykłąd dostałeś.
Mam wrażenie, że Twój wcześniejszy post jasno wspominał o interfejsie http ale z uwagi, że byłe dytowany to nie dam sobie głowy uciąć, że coś zbyt szybko przeczytałem.

0
RequiredNickname napisał(a):
Riddle napisał(a):

Jeśli tabelkę zasila coś innego (np przez jakąś inną aplikację), to ta tabelka również jest interfejsem aplikacji i również powinna być przetestowana. Taki test, jak rozumiem, według Twojej definicji również powinien być e2e.

Pytałeś o przykład i przykłąd dostałeś.
Mam wrażenie, że Twój wcześniejszy post jasno wspominał o interfejsie http ale z uwagi, że byłe dytowany to nie dam sobie głowy uciąć, że coś zbyt szybko przeczytałem.

Nie, napisałem o interfejsie aplikacji, nic nie wspominałem o interfejsie HTTP. Wysłałem Ci wiadomości prywatnej screena z historii edycji.

0
Riddle napisał(a):
slsy napisał(a):

Google ma dobry podział na testy https://testing.googleblog.com/2010/12/test-sizes.html , gdzie IMO integracyjne to odpowiednik medium a E2E large

Temu podziałowi to raczej bym nie ufał w kontekście dobrych testów, bo tam mają limit sekund na małe 60 sekund, a na duże 900.

Nie wiem co oni tam biorą, ale to na pewno nie jest definicja dobrych testów.

Ich system do budowania ogarnia ten podział. Możesz odpalić testy danego typu jak i te timeouty zabijają proces, jeśli trwają za długo

Można się spierac, ale przynajmniej ten podział jest jakoś formalnie sformalizowany i nie wynika od czyjegoś widzimisię

60 sekund na uruchomienie unit testów w jednym module/pakiecie brzmi poprawnie, ten limit zawsze też można zwiększyć https://www.virtuslab.com/blog/bazel-testing-configuration-comprehensive-guide/#bazel-testing-in-scala

0

I tak nie odzwierciedlisz stanu na produkcji - ja bym odpuścił, najwyżej wleci bug do sprintu.

2
cerrata napisał(a):

Hej, mam małe pytanka o testowanie integracyjne w Springu (pytania 1,2 w sumie ogólne):

  1. Czy testy integracyjne mogą współdzielić stan, czy po każdym powinienem czyściś/incijalizować DB ponownie? W sumie wydaje mi się, że szybciej byłoby raz wypełnić danymi niż przed każdym testem

Nie powinny, ponieważ testy integracyjne, to nadal testy, które obejmują jakąś specyficzną jednostkę systemu. Żeby miały sens, to powinny być powtarzalne, a to implikuje niezależność.

  1. Jaki jest najlepszy sposób dla wypełnienia DB przed testami - użyć normalnie usługi REST i wysłać żądania (np. utworzyć tak max kilka obiektów) czy jakoś inaczej wypełnić DB (np. adnotacja @Sql - https://docs.spring.io/spring-framework/reference/testing/annotations/integration-spring/annotation-sql.html)

Można np. użyć kombinacji testcontaines i liqiudbase. Za każdym razem otrzymasz świeżą bazę, w której będziesz miał tylko te dane, których potrzebujesz.

  1. Czy mam jakąś gwarancję, że kolejność wykonania moich testów będzie zawsze taka sama jeśli nie używam adnotacji @Order. Coś czytałem o jakiejś domyślnej kolejności, ale coś mi się nie zgadza jak patrzę na kolejność wykonania testów w Intellij (czy za pomocą przycisku w IDE, czy mvn clean install)

Nie masz gwarancji. Dobrze napisane testy nie powinny zależeć od siebie.

  1. Z tego co czytam, to domyślnie JUnit zakłada, że dla każdego testu instacja klasy jest tworzona ponownie. Ale jak rozumiem - nie dotyczy to automatycznego czyszczenia bazy (dla testów mam h2)? Po wykonaniu początkowych testów, które testują tworzenie - w kolejnych które testują odczyt np. listy obiektów widać te nowo utworzone w poprzednich testach

To raczej zły design testu. Nie dość, że uzależniasz jedne testy od drugich czasowo (kolejność wykonania), to jeszcze silnie wiążesz je stanem. Praktycznie nie masz możliwości wprowadzenia nowych funkcjonalności bez psucia całego kodu. Przy czym masz tutaj dość często spotykany błąd, który polega na rozdzieleniu funkcjonalności pod kątem konkretnej operacji, a nie scenariusza.
Przykład złego designu:

  • Test 1: Zapisz ABC do bazy
  • Test 2: Odczytaj zapisaną wartość ABC z bazy.
  • Test 3: Usuń wartość ABC z bazy.

I teraz pytanie, czy Test 2 nie jest przez przypadek asercją dla Test 1 i Test 3? Oraz kolejne, czy Test 1 nie jest przez przypadek założeniem dla Test 2 i Test 3.
Przykład poprawnego designu:

  • Test 1: Jeżeli baza jest pusta, to po zapisaniu wartości ABC, mogę ją odczytać.
  • Test 2: Jeżeli baza jest pusta, to po zapisaniu wartości ABC, zakładam, że baza nie jest pusta i mogę usunąć ABC i baza znowu jest pusta.

Można to przetłumaczyć na pseudo JUnita:


@Test
void test1(){
    Assumptions.assumeThat(repository.readAll()).isEmpty();
    repository.save("ABC")
    Assertions.assertThat(repository.readAll()).isNotEmpty().containsOnly("ABC"); // Zapis się udał i mogę odczytać to co trzeba
}


@Test
void test2(){
    Assumptions.assumeThat(repository.readAll()).isEmpty();
    repository.save("ABC")
    Assumptions.assumeThat(repository.readAll()).isNotEmpty(); // Zapis się zapewne udał, ale nie sprawdzam co zostało zapisane
    repository.delete("ABC")
    Assertions.assertThat(repository.readAll()).isEmpty(); // Usunięcie się udało, bo baza jest pusta
}

Założenia, to bardzo niedoceniany element testów, który znacząco ułatwia życie.

1 użytkowników online, w tym zalogowanych: 0, gości: 1