Granulacja testów integracyjnych

0

Zainspirowany tematem Odwrócona piramida testów postanowiłem sprawdzić to na własnym kodzie. No i w sumie przy pierwszym lepszym teście mam pewne wątpliwości, funkcjonalność jaką testuje to tworzenie użytkownika w systemie. Przetestować chcę to integracyjnie, czyli zacznam od strzału HTTP, kończę na odczytaniu wartości z bazy którą odpalam przez TestContainers. Co trzeba sprawdzić przy happy path?

  • kod odpowiedzi HTTP, czy jest 201
assertEquals(HttpStatus.CREATED, response.status)
  • czy w bazie mogę znaleźć użytkownika o id jakie dostałem w odpowiedzi
assertNotNull(userRepository.findById(response.body.userId, User.class))
  • czy dane przekazane w requeście zostały poprawnie zmapowane (savedUser to encja wyciągnięta z repozytorium po zapisie)
  assertEquals(request.name, savedUser.name)
  assertEquals(request.email, savedUser.email)
  • no i czy hasło zostało zakodowane
assertTrue(passwordEncoder.matches(user.password, savedUser.password))

No i proste pytanie - ile testów powinno powstać? 1, 2, 3, 4?

2

Wyznawcy cargo cultu napiszą, że jedna asercja na test. Według mnie testujesz jeden przypadek, a że kilka asercji - no trudno. Testujesz integracyjnie, więc nie jest zbrodnią, gdy sprawdzasz kilka rzeczy w jednym teście.

4

Pomysł jedna asercja na test nie ma sensu w takich testach (moim zdaniem w ogóle ma mało sensu, ale to inna bajka). Sprawdzasz wszystko co dany use-case miał pokazać i tyle. Jeśli to wymaga 2 albo 5 asercji to tyle ich właśnie będzie.
Patrz na to z perspektywy funkcji systemu. Jeśli dana funkcja zwraca kilka wyników albo ma kilka efektów, to musisz wszystkie sprawdzić i bez sensu byłoby odpalać identyczny test N razy za każdym razem z inną asercja :) Chyba ze płacą ci od liczby testów :D

1

@Gazel: na pewno powinien być to jeden test. Weź zwłaszcza pod uwagę że gdy poprawnie operacja zachodzi to możesz nie być wstanie ponowić testu z sukcesem - bo zajdzie zmiana w bazie danych i np. username będzie zajęty, więc choćby z tego powodu nie ma sensu inaczej ;)

2

Wyznawcy cargo cultu napiszą, że jedna asercja na test

To jest w zasadzie wada JUnit, który jest do czegoś takiego napisany - bo assert to exception - więc jak jeden wyleci to następnych nawet nie zobaczysz.
Całkiem nowoczesne frameworki testowe w jezykach funkcyjnych nie mają tego problemu.

Dla zainteresowanych - kotest w kotlinie nadal nie jest nowoczesny :-), i ma wadę jak wyżej, ale ze względu na składnię przynajmniej te wielokrotne asserty nie gryzą w oczy. No i można je rozłożyć po jednym na "test", ale tak żeby były kolejnymi asercjami w jednym scenariuszu.

W scali jest zio-test i flawless, ale oba tak nowoczesne, że aż eksperymentalne :-)

5

Od siebie dodam: Nie sprawdzaj bazy danych, to szczegół implementacyjny. Jeśli możesz wykonaj następny request HTTP i spróbuj udowodnić że user istnieje - np spróbuj go zalogować (tu zależy jak Twój system działa i jak możesz to sprawdzić)

2

Popieram @AreQrm :) Generalnie chcesz na tym poziomie testować funkcje systemu i spełnianie kontraktów, więc powinieneś operować jedynie publicznym API systemu.
Bebechów możesz dotykać jeśli np. musisz wygenerować jakiś "stan aplikacji" który jest ustalany "poza systemem" (np. jakieś dane w bazie nie są tworzone za pomocą twojego systemu, on tylko je czyta, w takiej sytuacji możesz potrzebować przed testem takie dane utworzyć), ale to by było na tyle.

1

Tyle, że zeby sprawdzić to wszystko, to musiałbym:

  • stworzyć użytkownika /api/user/create
  • zalogować się /api/user/authenticate by uzyskać token
  • pobrać jego dane /api/user/profile z uzyskanym tokenem

Czy to już nie wykracza poza granice jednego testu?

1

@Gazel a jak wygląda wymaganie funkcjonalne/use-case który chcesz przetestować? Czy aby czasem nie właśnie: Użytkownik może utworzyć konto a następnie się na nie zalogować? ;)
Samo użytkownik może utworzyc konto w systemie byłoby niemierzalne bo w jaki sposób chcesz weryfikować spełnienie tego wymagania? Chyba ze zakładasz ze odpowiedź 200 z serwera uznacz że się udało...

Zastanów się jak kazałbyś to przetestować ręcznemu testerowi ;)

0

Dokładnie tak. To co robisz, to nie tylko test integracyjny, ale.. brak mi słowa. Ja to nazywam 'functional test' - tak jest to nazywane w dokumentacji MS często. Test funkcjonalności?
W każdym razie, jeśli tak działa Twój system, to tak to testujesz. Nie zawsze wystarczy 1 call, czasem trzeba więcej.
Dodam, że jakby to był test z UI zamiast API, i pisany w Selenium, to robił by dokłądnie to samo. Najpierw tworzył użyszkodnika a później się logował. Ale widziałem już testy - któe się sypały przy każdym uruchomieniu, próbujące bawić się bazą bez potrzeby - to droga do nikąd.

0

Ok, teoretycznie eliminuje to potrzebę pisania testów dla samego logowania, bo będzie to zawarte już w tym teście.
No ale co z resztą przypadków? Np. użytkownik podał niepoprawny email przy próbie załozenia konta, wtedy również mam sprawdzić kolejno

  • http 400
  • brak możliwości zalogowania na to konto
  • brak wyników po zapytaniu o profil tego użytkownika ( w sumie tego nie mam jak zrobić bez tokena)
0

Tak. O ile takie testy chcesz napisać. Nie próbuj pisać dla każdego przypadku testu, bo do wielu rzeczy powinieneś mieć tez testy innego typu np jednostkowe.
Co do błędu walidacji, to sam response powinien wystarczyć, powinieneś dostać 400 i samo to powinno świadczyć że walidacja nie przeszla/ request jest niepoprawny - plus oczywiscie mozesz spr czy blad dotyczy pola z mailem. Takze tutaj nie musisz spr czy mozesz sie zalogowac jesli uzytkownik nie istnieje. Chociaz to tez ciekawy test by byl :D

0

@Gazel Sprawdzasz czy dostałeś 400 + że nie da się zalogować (o ile mocno nie ufasz temu systemowi) i tyle. Możesz mieć też inny test który sprawdza że ponowna próba rejestracji, tym razem z poprawnymi danymi przechodzi ok.

Nie próbuj pisać dla każdego przypadku testu, bo do wielu rzeczy powinieneś mieć tez testy innego typu np jednostkowe.

Tu się nie zgadzam. Moim zdaniem powinieneś mieć testy na wszystkie funkcje systemu opisane w wymaganiach. Jesli w wymaganiach masz ze po podaniu złego loginu leci 401 to taki test też należy napisać. Testy niższego rzędu są ok do sprawdzania czy kod działa tak mi się wydaje że działa. A tutaj mimo wszystko cały czas obracamy się na poziomie API systemu.

0

Dzięki za odpowiedzi, ostatnia seria pytań :)

Co z security? Poza endpointem do rejestracji i logowania, wszystko inne wymaga tokena. Wsadzić w @BeforeAll jakiegoś użytkownika do bazy i odpytać endpoint do logowania o jego token, który następnie reużyć w kolejnych testach? Czy sprawdzić logowanie w innym teście, a w innych testach w jakiś sposób to zamockować, czy skorzystać z profilu gdzie security jest wyłączone?

No i nazewnictwo... używam Junita5 więc mam do dyspozycji @DisplayName, ale jak nazwać taki test, który kolejno

  • tworzy użytkownika
  • loguje się jego danymi
  • odpytuje inny endpoint o jego dane by porównać z tym co podane zostało w punkcie 1

No i jeżeli decyduje się na taki test, gdzie olewam sprawdzanie bazy danych, jak w takim razie sprawdzę jeden z warunków, który mówi, że zapisane w bazie hasło ma być zashashowane?

#Edit
Co do punktu pierwszego, w sumie dodając coś takiego do każdego innego testu integracyjnego nie bardzo po co mam sprawdzać to tworzenie użytkownika w innym miejscu (poza sprawdzeniem scenariuszy innych niż ok)

    private lateinit var userToken: String

    @BeforeAll
    fun initUserToken() {
        val command = createUserCommand().copy(email = "[email protected]", password = "testPwd")
        post<CreateUserCommand>(REGISTER_ENDPOINT, command)
        this.userToken = post<AuthenticationResult>(AUTHENTICATE_ENDPOINT, AuthenticationCommand(command.email, command.password)).body.token
    }
0
  1. Nie. Absolutnie nie w before all, tylko w setupie testu! Taki tam shameless plug bo zrobiłem niedawno PoC https://github.com/Pharisaeus/almost-s3/blob/master/test/src/test/java/net/forprogrammers/almosts3/test/DownloadTest.java#L59
  2. A co chcesz sprawdzić w tym teście? Should register new user and allow logging-in
  3. To czy hasło w bazie będzie zahashowane (albo w ogóle że istnieje jakaś baza danych!) to jest wewnętrzny szczegół implementacji. Bo przecież możesz w ogóle nic nigdzie nie zapisywać, wszystko trzymać w pamięci a eventy zrzucać do loga i potem przy starcie aplikacji wczytywać znów tego loga eventów i odtwarzać stan. W efekcie taki szczegół implementacyjny sprawdzaj w teście szczegółów implementacyjnych czyli w jakimś teście twojego SQLRepository
  4. Twój edit -> patrz mój PoC i pomyśl o zrobieniu sobie takiego DSLa.

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