Konrad Kowalski (rootsher)Principal Platform & Reliability Architect001010111001110000101101001010000001100100100001

Integration tests: prawdziwa granica systemu

data
kategoria
Testing
także w
Backend · Databases
czytanie
2 min / 418 słów

Kod może być poprawny i nadal nie działać z bazą.

SQL może mieć literówkę w nazwie kolumny.

Transakcja może kończyć się za wcześnie.

Serializer może zapisywać datę inaczej, niż parser ją czyta.

Unit test tego nie widzi, bo jego zadaniem było odciąć zależności.

Integration test przywraca wybraną zależność i sprawdza prawdziwą granicę.

Co to za rodzaj testu

Integration test sprawdza współpracę dwóch lub więcej części systemu.

Najczęściej chodzi o granice typu:

text
aplikacja + baza danych
aplikacja + kolejka
repozytorium + migracje
endpoint + middleware
adapter + zewnętrzny format
worker + event

Nie chodzi o to, żeby uruchomić wszystko.

Chodzi o to, żeby przestać udawać tę zależność, w której naprawdę może pojawić się błąd.

Jeżeli ryzyko jest w SQL, test powinien używać prawdziwej bazy.

Jeżeli ryzyko jest w serializacji eventu, test powinien przejść przez prawdziwy serializer i parser.

Jeżeli ryzyko jest w middleware, test powinien przejść przez prawdziwy stos requestu.

Co się dzieje, kiedy ich nie ma

Bez integration tests system może mieć zielone unit tests i zepsute granice.

Każda część osobno wygląda dobrze.

Dopiero po złożeniu okazuje się, że:

text
kolumna ma inną nazwę
constraint odrzuca zapis
timezone zmienia datę
middleware nie ustawia usera
retry zapisuje rekord drugi raz
event nie przechodzi walidacji

To są błędy integracji, nie błędy pojedynczej funkcji.

Jeżeli łapie je dopiero E2E, diagnoza jest droższa.

Jeżeli łapie je dopiero produkcja, koszt jest już operacyjny.

Prawdziwa zależność, ale kontrolowany świat

Integration test powinien być możliwie prawdziwy na badanej granicy i możliwie sztuczny poza nią.

Dobry przykład:

text
Fastify app
PostgreSQL w kontenerze
migracje przed testem
fixture z minimalnymi danymi
request przez Supertest

Zły przykład:

text
cały staging
wspólna baza dla kilku zespołów
losowe dane z poprzednich runów
zależność od internetu

Pierwszy test sprawdza realną granicę.

Drugi test sprawdza pogodę w środowisku.

Fixtures i dane testowe

Integration test bez dobrych danych szybko robi się flaky.

Test powinien tworzyć tylko to, czego potrzebuje.

text
użytkownik
konto
zamówienie
rekord zależny

Jeżeli fixture jest za duża, test zaczyna dziedziczyć przypadkowe założenia.

Jeżeli fixture jest współdzielona, jeden test może zepsuć drugi.

Najbezpieczniejszy model to izolacja danych per test albo szybkie czyszczenie stanu między testami.

Nie zawsze jest to darmowe.

Ale brak izolacji kosztuje później więcej, bo flaky integration test jest jednym z najszybszych sposobów na zniszczenie zaufania do pipeline.

Narzędzie ze stacka

Do integration tests na backendzie w Node wybrałbym Testcontainers i Supertest.

Testcontainers daje prawdziwą bazę albo inną zależność bez ręcznego utrzymywania współdzielonego środowiska.

Supertest pozwala przejść przez prawdziwy request HTTP do aplikacji Fastify bez wystawiania jej na zewnętrzny port.

To dobry zestaw dla granicy:

text
Fastify + migracje + PostgreSQL + request HTTP

Na tej warstwie nie mockowałbym bazy, bo właśnie baza jest częścią ryzyka.

Kiedy integration test jest za duży

Integration test zaczyna być za duży, kiedy próbuje sprawdzić całą ścieżkę użytkownika.

Wtedy miesza kilka pytań:

text
czy API działa
czy baza działa
czy UI działa
czy routing działa
czy dane testowe są poprawne

To już jest bliżej E2E.

Integration test powinien mieć jedną ważną granicę.

Jeżeli nie umiem powiedzieć, jaka to granica, test prawdopodobnie jest źle ustawiony.

Następna warstwa to contract tests i API tests, czyli granice, które wychodzą poza jeden proces albo jedno repozytorium.