Testy w pipeline: kiedy unit, kiedy integration, a kiedy e2e

1
57
Rate this post

Nawigacja:

Po co układać strategię testów w pipeline zamiast „po prostu testować”

Dlaczego „jakieś testy” w CI/CD to za mało

W wielu zespołach testy w pipeline CI/CD istnieją tylko formalnie. Coś się uruchamia, coś czasem się wywali, ale niewiele z tego wynika. Jedni traktują czerwony pipeline jak techniczną ciekawostkę, inni jak uciążliwą przeszkodę, którą trzeba jak najszybciej „obejść”. Zaufanie do testów spada, a wraz z nim sens całej automatyzacji.

Kluczowy problem polega na tym, że bez przemyślanej strategii trudno odpowiedzieć na podstawowe pytania: które testy mają prawo zablokować wdrożenie, jakie rodzaje błędów mają być wykrywane na jakim etapie oraz ile czasu może to zająć. Gdy wszystko jest wrzucone do jednego worka „tests”, pipeline staje się nieprzewidywalny. Raz zielony, raz czerwony, bez jasnego powodu, z losowymi przerwami w delivery.

Strategia testów w pipeline to świadoma decyzja: co, kiedy i jak testować, aby wynik builda był realnym sygnałem jakości, a nie loterią. Dzięki temu zespół zaczyna traktować pipeline jako wspólne narzędzie do ochrony produkcji, a nie jako narzucony obowiązek z zewnątrz.

Obawy: wolny pipeline, ciągłe „czerwone” i frustracja zespołu

Najczęściej słyszane obawy przy wprowadzaniu bardziej rozbudowanych testów w CI/CD to:

  • „Pipeline będzie trwał za długo” – gdy każdy merge request będzie czekał kilkadziesiąt minut na wynik, deweloperzy zaczną szukać skrótów i wyłączeń.
  • „Ciągle świeci się na czerwono” – flakiness, złej jakości testy e2e, niestabilne środowiska testowe sprawiają, że czerwony status przestaje mieć znaczenie.
  • „Testy blokują funkcjonalny rozwój” – przy braku priorytetów testy próbują łapać wszystko naraz, zamiast skupić się na najbardziej krytycznych obszarach.

Te obawy są uzasadnione, ale w znacznej części wynikają z jednego: braku rozłożenia ciężaru na odpowiednie typy testów. Jeśli większość logiki weryfikowana jest dopiero w testach e2e, pipeline będzie zarówno wolny, jak i kruchy. Gdy ta sama logika zostanie objęta testami jednostkowymi i integracyjnymi, testy e2e mogą być nieliczne, szybkie i bardziej stabilne.

Od „mamy testy” do „pipeline realnie chroni produkcję”

Skuteczny pipeline nie tylko odpalają testy, ale przede wszystkim redukuje ryzyko regresji na produkcji. Różnica między „mamy testy” a „pipeline chroni produkcję” jest podobna do różnicy między posiadaniem alarmu w domu a faktycznym jego uzbrajaniem i reagowaniem na sygnały.

Pipeline, który realnie chroni produkcję, ma kilka charakterystycznych cech:

  • Przewidywalny czas wykonania – zespół wie, że wynik dla merge requestu dostanie w określonym, akceptowalnym czasie (np. 5–15 minut dla głównych testów).
  • Wyraźne quality gates – wiadomo, które typy testów blokują merge/deploy, a które są dodatkowymi sygnałami (np. raport jakościowy, ale bez twardego bloku).
  • Testy odzwierciedlają realne ryzyko biznesowe – najważniejsze ścieżki użytkownika i krytyczne reguły biznesowe są zabezpieczone przynajmniej dwoma warstwami: unit/integration i wybranymi scenariuszami e2e.

Tak zaprojektowany pipeline przestaje być dekoracją. Zespół wie, że przejście przez niego w kolorze zielonym znacznie ogranicza szansę awarii w produkcji – i to w konkretnych obszarach.

Przesuwanie kosztu błędów „w lewo”

Bez zautomatyzowanych testów w pipeline większość problemów wychodzi późno: na środowisku testowym, stagingu, a najgorzej – już po wdrożeniu. Koszt naprawy rośnie wtedy nie tylko przez konieczność hotfixów, ale także przez utratę zaufania użytkowników, pośpiech i bałagan w procesie wydawniczym.

Dobrze ułożona strategia testów w CI/CD pozwala przesunąć wykrywanie błędów jak najbliżej momentu ich wprowadzenia. Idealnie, jeśli zła zmiana zostanie złapana już na etapie commitu (pre-push hooks, szybkie testy jednostkowe), a najpóźniej na etapie głównego pipeline dla merge requestu.

Im wcześniej błąd zostanie znaleziony, tym taniej i spokojniej można go naprawić. Taka zmiana perspektywy – z „naprawiamy po fakcie” na „powstrzymujemy regresje zanim dotrą do produkcji” – jest jednym z największych zysków dobrze ułożonej piramidy testów w pipeline.

Co zrobić, gdy projekt już istnieje i „żyje własnym życiem”

W wielu zespołach sytuacja startowa jest daleka od idealnej: monolit bez testów, setki testów e2e z Selenium, niestabilne środowisko testowe, brak jasnych zasad. Zaczynanie od zera nie wchodzi w grę, bo system działa i musi działać dalej.

W takiej sytuacji sensowne jest podejście iteracyjne:

  • Krok 1: inwentaryzacja – spis istniejących testów (unit, integration, e2e), ocena ich czasu wykonania i awaryjności, identyfikacja duplikatów.
  • Krok 2: szybkie zwycięstwa – separacja najszybszych, najbardziej stabilnych testów jednostkowych i uruchomienie ich jako pierwszej warstwy w pipeline.
  • Krok 3: czyszczenie e2e – usuwanie lub refaktoryzacja oczywiście zbędnych, dublujących lub ekstremalnie niestabilnych scenariuszy e2e.
  • Krok 4: uzupełnianie warstwy integracyjnej – stopniowe dodawanie sensownych testów integracyjnych tam, gdzie brakuje pokrycia pomiędzy unit a e2e.

Transformacja strategii testów w istniejącym projekcie nie musi być rewolucją. Znacznie skuteczniejsze są małe, konsekwentne kroki, które krok po kroku poprawiają stabilność pipeline, zamiast zatrzymywać delivery na kilka tygodni „porządków technicznych”.

Krótkie przypomnienie: unit, integration, e2e – o co w nich chodzi praktycznie

Testy jednostkowe: najmniejszy klocek, największa dźwignia

Test jednostkowy (unit test) sprawdza pojedynczą, wyizolowaną jednostkę logiki – najczęściej funkcję, metodę, niewielki komponent. Chodzi o to, by dany fragment kodu zachowywał się w przewidywalny sposób dla określonych wejść, bez udziału zewnętrznych zasobów (bazy, sieci, systemu plików).

Kluczowe cechy testów jednostkowych:

  • Szybkość – dobrze napisane unity potrafią przejść w setkach lub tysiącach w ciągu sekund.
  • Izolacja – brak zależności od zewnętrznego środowiska; jeśli coś się psuje, problem jest albo w testowanym kodzie, albo w samym teście.
  • Deterministyczność – to samo wejście zawsze daje ten sam wynik, bez losowości i zależności od czasu.

Praktyczny przykład unit testu: funkcja licząca zniżkę dla koszyka na podstawie regulaminu promocji. Nie potrzebuje bazy, nie potrzebuje API – jedynie danych wejściowych i oczekiwanych rezultatów. Taki test daje jasny sygnał, czy logika biznesowa nadal jest zgodna z wymaganiami.

Testy integracyjne: jak elementy dogadują się ze sobą

Test integracyjny sprawdza współdziałanie kilku komponentów naraz – modułów w monolicie, serwisu i bazy danych, serwisu i message brokera, dwóch mikroserwisów komunikujących się po HTTP. Tu celem nie jest już izolacja, ale weryfikacja kontraktów i konfiguracji.

Charakterystyka testów integracyjnych:

  • Wolniejsze od unitów – wymagają podniesienia bazy, brokera, czasem całego kontenera z aplikacją.
  • Wrażliwość na środowisko – konfiguracja, migracje, dane początkowe mają znaczenie.
  • Sprawdzanie konfiguracji – czy mappingi ORM są poprawne, czy kolejki istnieją, czy API zwraca dane w oczekiwanym formacie.

Przykład: test, który uruchamia moduł aplikacji z prawdziwą bazą (np. w Dockerze) i sprawdza, czy zapis i odczyt zamówienia działa poprawnie, z uwzględnieniem transakcji, walidacji i indeksów. To jest już wyraźnie inny poziom niż sama logika biznesowa – tu liczy się interakcja z infrastrukturą.

Testy end-to-end: cały system oczami użytkownika

Test end-to-end (e2e) w kontekście pipeline CI/CD oznacza scenariusz przechodzący przez możliwie pełną ścieżkę użytkownika lub procesu biznesowego, najczęściej przez UI (przeglądarkę) albo publiczne API. Celem jest sprawdzenie, że wszystkie warstwy systemu – od frontendu, przez backend, po bazę i integracje zewnętrzne – razem realizują biznesowy cel.

Właściwości testów e2e:

  • Najwolniejsze – w grę wchodzą przeglądarka, sieć, wiele serwisów, czasem zewnętrzne API.
  • Najbardziej kruche – zależne od stabilności środowiska, danych testowych, opóźnień, kolejności uruchamiania.
  • Najbliżej realnego zachowania – dobrze napisany scenariusz e2e imituje to, co realnie robi użytkownik lub system zewnętrzny.

Przykład: użytkownik otwiera stronę sklepu, loguje się, dodaje produkt do koszyka, przechodzi przez checkout, otrzymuje potwierdzenie zamówienia i e-mail. Ten scenariusz dotyka wielu komponentów naraz i potwierdza, że krytyczna ścieżka zakupowa faktycznie działa.

To samo zachowanie, różne poziomy testów

To samo wymaganie biznesowe można sprawdzić na kilku poziomach. Na przykład: „Rabat 10% dla zamówień powyżej określonej kwoty”.

  • Unit – test funkcji calculateDiscount(), która na podstawie kwoty koszyka zwraca wysokość rabatu.
  • Integration – test modułu zamówień z prawdziwą bazą danych, który sprawdza, czy rabat jest poprawnie zapisywany razem z zamówieniem i naliczany również przy odczycie.
  • e2e – scenariusz UI lub API, w którym użytkownik faktycznie składa zamówienie, widzi naliczony rabat na podsumowaniu i otrzymuje fakturę z prawidłową kwotą.

Strategia testów w pipeline polega m.in. na tym, na którym poziomie zapewnić podstawową ochronę, a na którym tylko potwierdzić, że całość działa razem. Najczęściej opłaca się maksymalnie przesunąć logikę do unitów i wybranych testów integracyjnych, a e2e zostawić jako cienką, krytyczną warstwę.

Ekran laptopa z kodem i debugerem w środowisku developerskim
Źródło: Pexels | Autor: Daniil Komov

Piramida testów vs. „odwrócony lejek” – konsekwencje w CI/CD

Założenia piramidy testów

Klasyczna piramida testów mówi: najwięcej testów jednostkowych u podstawy, mniej testów integracyjnych pośrodku, najmniej testów e2e na szczycie. Ten układ ma bardzo konkretne uzasadnienie w kontekście CI/CD:

  • unity są szybkie i stabilne – można ich mieć setki lub tysiące, bez zabijania pipeline,
  • testy integracyjne są droższe – warto używać ich tam, gdzie faktycznie mają sprawdzać integracje, a nie dublować unitów,
  • e2e są najdroższe i kruche – powinny obejmować tylko krytyczne ścieżki.

Dobrze zaprojektowana piramida sprawia, że pipeline w większości przypadków przerywa się szybko. Jeśli błąd jest oczywisty (np. zepsuta logika biznesowa), złapią go unity. Jeśli to problem z konfiguracją lub bazą – testy integracyjne. Dopiero rzadkie problemy „na styku” całego systemu przechodzą do warstwy e2e.

Odwrócony lejek: gdy większość testów to e2e

W praktyce wiele projektów kończy z tzw. odwróconym lejkiem: niewiele unitów, trochę integracyjnych i ogromna liczba testów e2e przez UI. Konsekwencje dla CI/CD są szybko odczuwalne:

  • Pipeline trwa wieki – każdy merge wymaga przejścia setek scenariuszy e2e uruchamianych sekwencyjnie lub na ograniczonej liczbie agentów.
  • Flaky tests – im więcej testów e2e, tym większe ryzyko przypadkowych awarii niezwiązanych ze zmianą kodu.
  • Brak jasnej informacji zwrotnej – gdy pada e2e, często trudno od razu powiedzieć, czy zawinił frontend, backend, środowisko, dane testowe czy sam test.

Zespół przestaje ufać wynikom pipeline, bo „i tak się coś losowo wysypie”. Naturalną reakcją jest wyłączanie części testów, ignorowanie czerwieni lub obchodzenie pipeline (np. bezpośrednie deploye na produkcję), co znów zwiększa ryzyko awarii.

Wpływ struktury testów na czas i stabilność pipeline

Stosunek liczby testów na poszczególnych poziomach wprost przekłada się na czas wykonania i stabilność pipeline. Dlatego przy projektowaniu strategii testów warto myśleć nie tylko o „pokryciu”, ale i o koszcie uruchomienia każdego typu testów.

Prosty sposób patrzenia na to zagadnienie:

Progi w pipeline: jak zdecydować, co uruchamiać kiedy

Najczęstszy dylemat przy projektowaniu pipeline nie brzmi „czy pisać testy?”, tylko: co odpalać przy każdym commicie, a co zostawić na później. Jeśli do tej pory wszystko leciało zawsze „na raz”, nic dziwnego, że buildy trwają pół godziny i wszyscy się frustrują.

Pomaga myślenie o pipeline jako o ciągu coraz droższych progów, które kod musi pokonać, żeby dolecieć do produkcji. Im wyższy próg, tym mniej zmian do niego dociera.

  • Próg 1 – testy lokalne / pre-commit: podstawowe unity, lint, szybkie checki typów. Idealnie, jeśli da się je uruchomić w sekundach na laptopie.
  • Próg 2 – CI na branchu: pełny zestaw testów jednostkowych i sensowna część integracyjnych. Celem jest wczesne odrzucenie większości problemów.
  • Próg 3 – pre-merge / pre-release: pełny pakiet integracyjny i — w zależności od projektu — podzbiór e2e (np. tylko najbardziej krytyczne ścieżki).
  • Próg 4 – nightly / regresja: szeroka siatka e2e i ciężkie scenariusze, które nie muszą chodzić przy każdym merge’u.

Takie podejście odczarowuje pytanie „kiedy uruchamiać e2e?”. Zamiast jednej, magicznej odpowiedzi pojawia się bardzo praktyczne: „zawsze, ale nie zawsze wszystkie”.

Heurystyka: jaki typ testu dla jakiej zmiany

Kiedy kod jest mały i zespół się zna, decyzje o poziomach testów często są intuicyjne. W większych projektach pomaga zestaw prostych zasad – nie po to, by kogoś ograniczać, tylko by ujednolicić oczekiwania.

Przykładowa heurystyka, którą da się wdrożyć bez rewolucji:

  • Zmiana w czystej logice domenowej (np. kalkulacje, walidacje, reguły biznesowe):
    minimum: nowe lub zmienione testy jednostkowe,
    opcjonalnie: integracyjne tylko wtedy, gdy logika „przecieka” do warstwy bazy, kolejek itp.
  • Zmiana w dostępie do danych / konfiguracji infrastruktury (repozytoria, ORM, connection stringi, mapowania):
    minimum: testy integracyjne sprawdzające zapis/odczyt lub komunikację,
    — unity mają sens głównie przy skomplikowanych mapperach czy transformacjach.
  • Zmiana w kontraktach API / komunikacji między serwisami:
    minimum: testy kontraktowe / integracyjne dla obu stron,
    — e2e potrzebne tylko, jeśli zmiana dotyka krytycznej ścieżki biznesowej.
  • Zmiana w UI bez zmiany logiki backendu:
    minimum: testy komponentów (często traktowane jak „wyższe unity”) i snapshoty,
    — e2e tylko wtedy, gdy dotyczy to głównych scenariuszy użytkownika (np. proces zakupu, logowanie).

Jeśli zespół ma taką „mapę”, łatwiej bronić decyzji: czemu w tym zadaniu powstały wyłącznie unity, a gdzie indziej dokładane są od razu e2e.

Kiedy zatrzymać się na unitach, a kiedy iść wyżej

Sytuacje, w których unit testy w zupełności wystarczą

Nie każdy fragment systemu potrzebuje pełnej orkiestry testów. Są miejsca, gdzie mocne pokrycie unitami daje prawie cały zysk, a dokładanie integracji czy e2e nie wnosi wiele.

Typowe obszary:

  • Czysta logika domenowa, bez pobocznych efektów: liczenie rabatów, reguły scoringu, walidacje formularzy, silniki reguł.
  • Funkcje pomocnicze: parsery, formattery, konwertery jednostek, transformacje danych.
  • Proste komponenty prezentacyjne, które głównie renderują dane w określony sposób, bez złożonych interakcji.

W tych miejscach integracja z infrastrukturą często nie istnieje albo jest tak prosta, że test integracyjny nic nie dodaje. Dużo większą dźwignię daje rozbudowanie scenariuszy unitowych: dane brzegowe, scenariusze błędne, nietypowe kombinacje parametrów.

Sygnalizatory, że samych unitów jest za mało

Jeżeli pojawiają się takie symptomy:

  • „U nas wszystko zielone, ale na testach integracyjnych innego zespołu co chwilę coś pada”.
  • „Na localu przechodzi, na środowisku testowym wyjątek z bazy / brokera / konfiguracji”.
  • „Zmiana w mapowaniu encji rozwaliła pół raportowania, a unity milczą”.

— to zwykle znak, że brakuje testów wyższego poziomu. Logika jest sprawdzona, ale jej „dostarczenie” do bazy, innego serwisu czy UI już nie.

W takiej sytuacji najpierw sięga się po testy integracyjne, bo to one łapią problemy z ORM, migracjami, kolejkami, konfiguracją. E2e zostawia się na koniec, gdy potrzebne jest potwierdzenie, że cała ścieżka, od kliknięcia w UI po zapis w bazie, działa w komplecie.

Jak sensownie dobrać testy integracyjne

Integracja, ale czego z czym?

Termin „test integracyjny” bywa tak szeroki, że każdy rozumie go inaczej. Dla pipeline ważne jest, by były one konkretne i powtarzalne. Dużo pomaga odpowiedź na proste pytanie: „Jaką integrację faktycznie weryfikujemy?”.

Najczęstsze typy integracji w praktyce:

  • Serwis ↔ baza danych: poprawność zapisu/odczytu, transakcje, indeksy, migracje.
  • Serwis ↔ message broker: publikacja i konsumpcja zdarzeń, format payloadu, routing.
  • Serwis ↔ serwis (HTTP, gRPC): zgodność kontraktów, kody odpowiedzi, mapowanie błędów.
  • Serwis ↔ zewnętrzne API: tutaj częściej stosuje się testy kontraktowe z mockiem dostawcy niż „prawdziwe” połączenia.

Jeśli test integracyjny nie potrafi odpowiedzieć jasno: „co dokładnie sprawdzam?”, zwykle zaczyna dryfować w stronę pół-e2e — długi, niestabilny, a dalej bez jasnego sygnału, gdzie leży problem.

Strategia: wąskie, ale głębokie testy integracyjne

Dobre testy integracyjne są wąskie w zakresie (dotykają jednego modułu / serwisu), ale głębokie w tym, jak przechodzą przez infrastrukturę. Przykładowa strategia dla serwisu obsługującego zamówienia:

  • Zamiast jednego, ogromnego testu „od stworzenia zamówienia po wystawienie faktury” — kilka węższych:
    — zapis i odczyt zamówienia w bazie (różne stany, pola opcjonalne),
    — publikacja zdarzenia „OrderCreated” do brokera i jego poprawny format,
    — obsługa zdarzenia „PaymentConfirmed” i aktualizacja statusu.
  • Do tego proste dane startowe: minimalny seed bazy i broker „czysty” na starcie każdego scenariusza.

Takie testy są już droższe niż unity, ale dają konkretną informację: „nie działa zapis w bazie”, „event ma zły format”, „konsument się wywala przy duplikatach”. E2e mogą później tylko potwierdzić, że cały proces biznesowy z tych klocków się składa.

Zbliżenie monitora z kodem i danymi bezpieczeństwa w pipeline testowym
Źródło: Pexels | Autor: Tima Miroshnichenko

Testy e2e: kiedy są naprawdę potrzebne

Kryteria włączenia scenariusza do zestawu e2e w CI

Jeśli w projekcie e2e rozmnożyły się bez kontroli, trudno potem powiedzieć, które są faktycznie potrzebne. Przy porządkach pomaga prosty filtr. Do stałego zestawu e2e w pipeline dobrze trafiają scenariusze, które:

  • Dotyczą kluczowego przychodu lub ryzyka: ścieżka zakupowa, logowanie, reset hasła, potwierdzenie przelewu, wysyłka krytycznych powiadomień.
  • Przekraczają kilka domen lub serwisów: zmiana statusu zamówienia uruchamia wysyłkę maila i zapis do systemu fakturowania.
  • Nie dają się dobrze pokryć niższymi poziomami: np. interakcje w UI, które są wynikiem kombinacji wielu warstw.
  • Mają historię regresji: miejsce, które „ciągle się psuje”, dopóki nie zostanie realnie zabezpieczone.

Reszta scenariuszy e2e nie musi od razu wylecieć do kosza. Część można:

  • przekształcić w testy API (bliżej integracji niż pełnego e2e),
  • odpalać rzadziej (nightly, przed dużym releasem),
  • rozbić na kilka integracyjnych, jeśli głównym celem jest np. weryfikacja mapowań lub przepływu danych.

Stabilność e2e a zaufanie do pipeline

Główny problem z e2e nie polega na tym, że są wolne, tylko że często są niemożliwe do zdiagnozowania. Test raz przechodzi, raz nie. Raz nie wystartował kontener, innym razem timeout, jeszcze innym brak danych startowych.

Aby scenariusze e2e w pipeline miały sens, potrzebują kilku trwałych zasad:

  • Izolacja danych — każdy test przygotowuje sobie dane i sprząta po sobie (lub korzysta z resetu środowiska między scenariuszami).
  • Brak zależności od kolejności — scenariusze nie polegają na tym, że „poprzedni test założył użytkownika”.
  • Wyraźne time-outy i retry tam, gdzie to realne — system faktycznie może mieć opóźnienia, ale nie powinien wisieć 3 minuty bez informacji.
  • Ograniczony zakres asercji — test skupia się na istotnym zachowaniu, a nie na każdym pixelu w UI, który zmienia się co sprint.

Kiedy te zasady są spełnione, nawet stosunkowo mały zestaw e2e zaczyna być naprawdę wartościowy: jeśli coś upadło, jest to sygnał do szybkiej reakcji, a nie „kolejny flaky”.

Przykładowa architektura pipeline z podziałem na typy testów

Etap 1: szybka weryfikacja na każde push

Cel pierwszego etapu: nie marnować czasu na drogie kroki, jeśli kod ma oczywiste błędy. Typowy zestaw:

  • lint, formatowanie, statyczna analiza,
  • pełny zestaw testów jednostkowych,
  • opcjonalnie: kilka najbardziej krytycznych testów integracyjnych (np. smoke test bazy).

Ten etap powinien kończyć się w minutach, nie w dziesiątkach minut. Jeśli trwa zbyt długo, zwykle unity są zbyt ciężkie (np. korzystają z bazy) albo do tego etapu „przemyciły się” scenariusze integracyjne czy e2e.

Etap 2: testy integracyjne na merge request

Gdy kod przejdzie pierwsze sito, można pozwolić sobie na coś droższego. Dla każdego merge requestu (lub dla wybranych gałęzi) seria:

  • uruchomienie usług zależnych (baza, broker, serwisy pomocnicze) w kontenerach,
  • pełny zestaw testów integracyjnych,
  • ewentualne lekkie testy API bez UI (np. smoke testy głównych endpointów).

Jeśli ten etap wykazuje dużo flakiness, często winne jest środowisko: brak izolacji danych, współdzielone zasoby, brak deterministycznego startu kontenerów. Uporządkowanie tych elementów zazwyczaj daje większy zysk niż samo „dokładanie kolejnych testów”.

Etap 3: wąski zestaw e2e przed deployem

Przed deployem do środowiska testowego lub produkcji dobrze sprawdza się niewielki, ale stabilny pakiet scenariuszy e2e:

  • 2–5 najbardziej krytycznych ścieżek użytkownika,
  • scenariusze bezpieczeństwa i autoryzacji (np. brak dostępu do cudzych danych),
  • najważniejsze integracje z systemami zewnętrznymi, o ile da się je odtworzyć.

Ten etap nie musi uruchamiać wszystkich e2e, jakie kiedykolwiek napisano. Może też działać na osobnym środowisku, bardziej zbliżonym do produkcji. Kluczowe jest, by w przypadku niepowodzenia dawać czytelny sygnał: „lepiej wstrzymać releasę i sprawdzić, co się dzieje”.

Etap 4: cykliczne, ciężkie testy regresyjne

Ostatni element układanki to miejsce na wszystko, co ważne, ale zbyt kosztowne, by uruchamiać przy każdym commicie czy merge’u:

  • pełne regression e2e (większa liczba scenariuszy UI/API),
  • długotrwałe scenariusze, np. procesy wsadowe, batch processing,
  • testy obciążeniowe i wydajnościowe.

Taki zestaw dobrze odpalać np. nightly albo przed dużymi releasami. Wynik tych testów nadal jest ważny dla decyzji biznesowej, ale nie blokuje codziennego przepływu małych zmian.

Decyzje w codziennej pracy: co dopisać, a czego nie dublować

Dodawanie testów do istniejącego kodu

Najbardziej kłopotliwa jest sytuacja, gdy pipeline już istnieje, kod żyje od lat, a testy są cienkie jak papier. Naturalna obawa: „jeśli zacznę dopisywać testy, to utoniemy w refaktorach i opóźnieniach”. Da się to ogarnąć małymi krokami, bez rewolucji.

Dobry schemat na start to podejście „dotykam → zabezpieczam”:

  • przed większą zmianą w danym fragmencie kodu dopisujesz przynajmniej jeden unit i jeden sensowny integracyjny (jeśli ten fragment ma styczność z infrastrukturą),
  • nie przerabiasz od razu całego modułu — zabezpieczasz dokładnie to, co za chwilę zmodyfikujesz.

Przykład z praktyki: dodajesz nowe pola do encji „Invoice” i zmieniasz sposób naliczania rabatów. Zamiast planu „napiszmy 50 testów, które pokryją wszystko”, można podejść tak:

  • kilka unitów dla logiki rabatów (różne kombinacje stawek, podatków, progów),
  • jeden test integracyjny, który sprawdza, że faktura z rabatem zapisuje się w bazie z poprawnymi wartościami i przechodzi przez migracje.

Z biegiem czasu, przy każdym dotknięciu kolejnych fragmentów, pokrycie rośnie tam, gdzie faktycznie jest ryzyko. Bez paraliżu i konieczności „zamykania projektu na 3 miesiące testów”.

Kiedy wystarczy unit, a kiedy warto od razu celować w integracje

Granica między „jeszcze unit” a „to już integracja” bywa myląca, szczególnie gdy kod aplikacyjny jest mocno spleciony z infrastrukturą. Pomaga prosty filtr: czy logika, którą zmieniasz, ma sens bez realnych zależności?

Jeśli odpowiedź brzmi „tak”, pierwszeństwo mają unity:

  • algorytmy: ceny, rabaty, reguły walidacji, mapowanie DTO → model domenowy,
  • transformacje danych: parsowanie plików, agregacje, sortowanie, filtrowanie,
  • logika decyzyjna: „czy można złożyć zamówienie”, „jaki status przypisać”.

Gdy jednak sednem zmiany jest przepływ przez infrastrukturę, od razu opłaca się sięgnąć po integracje:

  • nowa tabela lub kolumny w bazie, migracje, nietypowe indeksy,
  • nowa kolejka, routing do innego exchange, zmiana formatu eventu,
  • nowy endpoint HTTP lub istotna zmiana kontraktu (np. wprowadzenie paginacji).

Unit może tu być przydatnym dodatkiem (np. sprawdza reguły mapowania), ale to test integracyjny da odpowiedź na pytanie, czy aplikacja dogaduje się z realnym środowiskiem.

Jak nie dublować testów między poziomami

Częsty ból: ten sam scenariusz jest testowany jako unit, integracja i e2e. Wszystko przechodzi, dopóki nie zacznie brakować zasobów i cierpliwości. Podział odpowiedzialności między poziomami mocno upraszcza sytuację.

Sprawdza się zasada „każdy poziom ma swoją rolę”:

  • Unit — logika i krawędzie (edge cases). Wyszukuj dziwne kombinacje danych, warunki brzegowe, wyjątki.
  • Integracja — kontrakty i konfiguracja. Czy coś zapisuje się tam, gdzie trzeba? Czy event ma dokładną strukturę, którą rozumie konsument? Czy transakcje działają w praktyce?
  • E2e — najważniejsze ścieżki użytkownika i „szwy” między systemami. Nie wchodź tu w szczegóły mapowania pól, skup się na tym, czy cel biznesowy został osiągnięty.

Na prostym przykładzie „reset hasła” może wyglądać to tak:

  • unit: generowanie tokenu, sprawdzanie daty ważności, walidacja siły nowego hasła,
  • integracja: zapis tokenu w bazie, jego użycie i unieważnienie, komunikacja z systemem mailowym (np. przez message broker lub HTTP),
  • e2e: użytkownik klika „Zapomniałem hasła”, dostaje maila z linkiem, ustawia nowe hasło i może się zalogować.

Jeśli e2e zaczyna testować te same reguły, które są już solidnie pokryte unitami (np. minimalną długość hasła), łatwo zrobić z niego niestabilnego potwora. Wtedy lepiej odpuścić szczegóły na najwyższym poziomie i zostawić tu tylko weryfikację „reset faktycznie działa od końca do końca”.

Reakcja na regression bug: co dopisać, gdzie i dlaczego

Kiedy coś ucieknie na produkcję, naturalny odruch to dopisać „jeszcze jednego e2e, żeby się więcej nie powtórzyło”. Nie zawsze to najlepsza inwestycja. Sensowniej zacząć od pytania: „na jakim najniższym poziomie ten błąd byłby najłatwiejszy do złapania?”.

Dobry schemat reagowania na regression bug:

  1. Odtworzyć błąd lokalnie i zrozumieć, w którym miejscu kodu faktycznie się ujawnia.
  2. Najpierw dopisać test na najniższym możliwym poziomie, który ten błąd ujawnia:
    • jeśli to czysta logika — unit,
    • jeśli problem wynikał z konfiguracji/integracji — integracja,
    • jeśli błąd był wyłącznie kombinacją wielu serwisów i UI — dopiero wtedy e2e.
  3. Jeśli błąd dotyczył krytycznej ścieżki biznesowej, dopiero po zabezpieczeniu niższych poziomów rozważyć dodatkowy scenariusz e2e jako siatkę bezpieczeństwa.

Przykład: faktury z określonego kraju wystawiały się bez podatku. Okazało się, że w jednym serwisie mapowanie kodu kraju z API na model domenowy miało błąd w literówce. Zamiast dokładania nowego, ciężkiego e2e „wystaw fakturę z kraju X i sprawdź VAT”, mocniejsze było:

  • unit: przetestowanie mapowania kodów krajów na stawki VAT,
  • integracja: zapis faktury z danym kodem kraju i sprawdzenie, że w bazie leży poprawna wartość podatku.

Dopiero jeśli taki błąd dotknął kluczowej dla firmy funkcji finansowej, sens ma dodatkowy krótki e2e, który przechodzi przez ścieżkę fakturowania jako całość.

Jak dobierać poziom testu do typu zmiany

Żeby codzienne decyzje były szybsze, pomaga prosty „routing” zmian do typów testów. Można go nawet spisać w zespole jako krótką ściągę.

Przykładowa matryca:

  • Zmiana UI bez modyfikacji logiki biznesowej:
    • unit: minimalnie lub wcale (chyba że UI zawiera własną logikę, np. formularz z walidacją),
    • integracja: zwykle bez zmian (backend nie drgnął),
    • e2e: dostosowanie istniejących scenariuszy UI tak, by asercje były odporne na kosmetykę (np. mniej sprawdzania CSS, więcej zachowania).
  • Zmiana logiki biznesowej bez zmian kontraktów:
    • unit: główny ciężar, dużo scenariuszy, warunki brzegowe,
    • integracja: wybrane przypadki, które przechodzą przez bazę/zdarzenia (czy wszystko dobrze się zapisuje i publikuje),
    • e2e: zwykle bez nowych scenariuszy, ewentualnie rozszerzenie istniejących o nowe warianty (np. dodatkowy typ użytkownika).
  • Zmiana kontraktów między serwisami / zewnętrznym API:
    • unit: testy mapowań do nowych struktur, obsługa błędów,
    • integracja: testy kontraktowe, scenariusze z prawdziwym (lub wiernie odtworzonym) API,
    • e2e: przynajmniej jeden scenariusz, który przechodzi przez nową interakcję w kontekście kluczowej ścieżki użytkownika.
  • Zmiany w infrastrukturze (nowa wersja bazy, inny broker, przejście na HTTPS wszędzie):
    • unit: zwykle bez zmian, dopóki API kodu aplikacyjnego się nie zmienia,
    • integracja: główny obszar inwestycji — sprawdzenie migracji, połączeń, parametrów, retry,
    • e2e: kilka smoke testów najważniejszych ścieżek, uruchamianych po zmianie środowiska.

Taki prosty routing zmniejsza liczbę dyskusji „czy do tego potrzebujemy e2e?”, bo zespół ma wspólne oczekiwania wobec każdego typu zmiany.

Jak rozmawiać o testach w zespole, żeby nie utknąć

Największe konflikty wokół testów zwykle nie wynikają z narzędzi, tylko z różnych oczekiwań. Jedna osoba chce mieć wszystko pokryte e2e „dla bezpieczeństwa”, inna broni się przed każdą dodatkową minutą w pipeline. Można to rozbroić kilkoma prostymi zasadami zespołowymi.

Pomaga np. wprowadzenie jawnych kryteriów „kiedy który test”, spisanych w kilku punktach, a nie w 50-stronicowym dokumencie. Do tego konkretne umowy:

  • Wspólny język — doprecyzowanie, co w tym projekcie znaczy „test integracyjny”, „e2e”, „test API”. Każdy projekt wypracowuje własne definicje, ważne, żeby wszyscy grali do tej samej bramki.
  • Code review z myślą o poziomach testów — przy przeglądzie zmian pojawia się pytanie: „czy to zabezpieczenie na właściwym poziomie?”. Recenzent nie tylko sprawdza, czy test „jest”, ale czy nie duplikuje innego i czy nie powinien być przeniesiony niżej/wyżej.
  • Bez obwiniania za bugi „bez testu” — zamiast polowania na winnych, lepiej razem ustalić, jaki nowy test (i na jakim poziomie) zabezpieczy podobne sytuacje. To obniża lęk przed przyznaniem się do pomyłki i ułatwia rozmowę o jakości.

Dzięki temu decyzje o dopisywaniu testów mniej przypominają przepychanki, a bardziej wspólne dbanie o to, żeby pipeline rzeczywiście dawał spokój głowy, a nie był tylko rytuałem „bo tak trzeba”.

Stopniowe przenoszenie ciężaru z e2e na integracje i unity

W wielu zespołach start jest odwrotny niż by się chciało: mało unitów, garstka integracji i wielki stos e2e, które trzymają jako tako całość w ryzach. Da się jednak bezboleśnie odwrócić ten stos, zamiast od razu wycinać wszystko, co drogie.

Praktyczny plan krok po kroku:

  1. Spisz listę istniejących e2e z krótkim opisem: jaka ścieżka, które serwisy, jak krytyczna biznesowo.
  2. Oznacz najcięższe i najbardziej flaky scenariusze — to najczęściej dobrzy kandydaci do rozbicia na integracje.
  3. Dla każdego z nich odpowiedz:
    • „Jakie konkretnie integracje on weryfikuje?” (baza, kolejki, serwisy zewnętrzne),
    • „Które elementy są już pokryte unitami/integracjami, a tylko przypadkiem zahaczane przez to e2e?”.
  4. Dopisz brakujące testy integracyjne, które pokrywają kluczowe „szwy” — zapis w bazie, eventy, wywołania API. Dopiero gdy masz je na miejscu, możesz bez stresu ograniczyć e2e do węższego zakresu.
  5. Stopniowo redukuj asercje w e2e (lub liczbę scenariuszy), zostawiając tam tylko weryfikację głównej ścieżki biznesowej.

W praktyce często kończy się tak, że zamiast 30 ciężkich e2e zostaje 5–10 naprawdę ważnych, a pozostałe zachowania przejmują tańsze integracje i unity. Efekt uboczny: pipeline przyspiesza, a diagnozowanie błędów staje się znacznie prostsze, bo sygnał z testu jest bardziej konkretny.

Najczęściej zadawane pytania (FAQ)

Jak ułożyć strategię testów w pipeline CI/CD, żeby nie był wiecznie czerwony?

Na początek rozdziel testy według typów i ról: szybkie, stabilne testy jednostkowe jako pierwsza linia obrony; testy integracyjne jako druga warstwa; nieliczne, dobrze dobrane scenariusze e2e jako ostatni filtr przed produkcją. Samo wrzucenie wszystkiego do jednego kroku „tests” zwykle kończy się loterią i utratą zaufania do pipeline’u.

Dobrą praktyką jest ustalenie quality gates: które testy mogą zablokować merge/deploy (np. unit + krytyczne integration), a które są tylko sygnałem (np. dodatkowe raporty, długie testy e2e odpalane nocą). Dzięki temu czerwony status ma jasne znaczenie, a zespół nie uczy się go „omijać” czy ignorować.

Kiedy używać testów jednostkowych, a kiedy integracyjnych w CI/CD?

Testy jednostkowe uruchamiaj zawsze i jak najwcześniej: w pre-push hookach, w głównym pipeline dla każdego merge requestu. Dobrze napisane unity są bardzo szybkie, niezależne od środowiska i idealnie nadają się do łapania błędów logiki biznesowej tuż po commicie.

Testy integracyjne dodaj tam, gdzie zaczynasz korzystać z prawdziwej infrastruktury: bazy danych, brokera wiadomości, komunikacji między serwisami. To one wychwytują błędy konfiguracji, mappingów, kontraktów API. Nie muszą obejmować całej logiki – mają potwierdzić, że klocki techniczne poprawnie ze sobą współpracują.

Ile testów e2e w pipeline to „wystarczająco”, żeby chronić produkcję?

Najczęściej mniej niż intuicyjnie się zakłada. W codziennym pipeline dla merge requestów zwykle wystarczy kilka–kilkanaście stabilnych scenariuszy e2e, które przechodzą przez kluczowe ścieżki użytkownika i krytyczne procesy biznesowe (np. rejestracja, zakup, płatność, krytyczne workflow back-office).

Rozbudowane, długie zestawy e2e lepiej uruchamiać rzadziej: np. cyklicznie w nocy, przed większym wydaniem lub na dedykowanym środowisku. Gdy większość logiki jest pokryta testami unit i integration, e2e nie muszą sprawdzać wszystkiego – mają upewnić się, że „całość wstaje” i kluczowe ścieżki naprawdę działają z perspektywy użytkownika.

Jak przyspieszyć wolny pipeline testowy, nie rezygnując z jakości?

Najpierw zmierz, co faktycznie spowalnia pipeline: osobno czas testów unit, integracyjnych i e2e. Zwykle okazuje się, że największym problemem są ciężkie scenariusze e2e i niepotrzebnie odpalane pełne środowiska testowe. Czasem proste rozdzielenie kroków (unit osobno, integration osobno, e2e osobno) daje już dużą poprawę.

Przyspieszenie można osiągnąć przez: równoległe uruchamianie testów, cache’owanie zależności, skrócenie zestawu e2e do krytycznych przypadków oraz przeniesienie jak największej części logiki z e2e do warstwy unit/integration. Dobrym celem jest taki układ, w którym główny feedback dla merge requestu pojawia się w ciągu 5–15 minut.

Co zrobić, gdy projekt jest stary, ma tysiące testów e2e i chaos w pipeline?

W takiej sytuacji lepiej unikać rewolucji typu „wyrzucamy wszystko i piszemy od nowa”. Sprawdza się podejście krok po kroku: najpierw inwentaryzacja testów (co istnieje, jak długo trwa, jak często pada), potem wyciągnięcie na wierzch najszybszych, stabilnych testów i uruchomienie ich jako pierwszej warstwy pipeline’u.

Kolejny etap to porządkowanie: usuwanie oczywistych duplikatów e2e, wyłączanie ekstremalnie niestabilnych scenariuszy lub przenoszenie ich do rzadziej uruchamianych jobów. Równolegle można dokładać brakujące testy integracyjne, które przejmą ciężar weryfikacji technicznej od e2e. Dzięki temu delivery nie staje, a pipeline z tygodnia na tydzień jest bardziej przewidywalny.

Jak przesunąć wykrywanie błędów „w lewo” w kontekście CI/CD?

Chodzi o to, by błąd był wychwycony jak najbliżej miejsca powstania – najlepiej jeszcze zanim trafi do głównej gałęzi. Pomaga w tym uruchamianie szybkich testów jednostkowych lokalnie (np. w pre-commit lub pre-push hookach) oraz lekkich pipeline’ów dla feature branchy, które dają szybki feedback bez podnoszenia całego środowiska.

W praktyce dobrze działa podział: super szybki zestaw (lint + podstawowe unity) uruchamiany zawsze i pełniejszy zestaw (unity + integration + krytyczne e2e) dla merge requestów do głównej gałęzi. Im więcej logiki da się pokryć na tych wcześniejszych warstwach, tym rzadziej błąd „przemyka” aż do stagingu czy produkcji.

Najważniejsze wnioski

  • Same „jakieś testy w CI/CD” nie wystarczą – bez świadomej strategii pipeline staje się losowy, zespół ignoruje czerwony status i automatyzacja przestaje realnie chronić produkcję.
  • Kluczem jest jasne rozłożenie odpowiedzialności między typami testów: większość logiki powinna być w unitach i integracjach, a e2e tylko dla kluczowych ścieżek, dzięki czemu pipeline jest szybszy i stabilniejszy.
  • Obawy typu „pipeline będzie za wolny” czy „ciągle czerwono” wynikają zwykle z przeciążenia warstwy e2e oraz niestabilnych środowisk – uporządkowanie piramidy testów znacząco zmniejsza frustrację zespołu.
  • Skuteczny pipeline ma przewidywalny czas wykonania, zdefiniowane quality gates (które testy blokują merge/deploy), a testy odzwierciedlają realne ryzyka biznesowe, np. krytyczne ścieżki użytkownika zabezpieczone na kilku poziomach.
  • Przesunięcie wykrywania błędów „w lewo” – jak najbliżej commitu – radykalnie obniża koszt naprawy; szybkie testy jednostkowe i podstawowe integracje powinny łapać większość regresji jeszcze przed głównym pipeline.
  • Nawet w „zastałych” projektach (monolit, masa kruchych e2e) zmiana jest możliwa małymi krokami: inwentaryzacja testów, wydzielenie szybkich unitów, ograniczenie duplikujących e2e i stopniowe budowanie sensownej warstwy integracyjnej.
Poprzedni artykułIPv6 w domu i firmie: konfiguracja, pułapki i testy łączności
Następny artykułOszustwa na OLX i Vinted: najczęstsze triki i jak się bronić
Aleksandra Michalski
Aleksandra Michalski pisze o trendach technologicznych: IoT, 5G, startupach i tym, jak innowacje przekładają się na codzienne zastosowania. Łączy research z krytycznym spojrzeniem na marketingowe obietnice, porównując dane z raportów, standardów i doświadczeń użytkowników. W artykułach dba o kontekst: koszty wdrożenia, ryzyka prywatności, kompatybilność i realną dojrzałość rozwiązań. Zamiast prognoz „na wyrost” stawia na scenariusze i praktyczne kryteria wyboru. Jej styl to klarowne wnioski, ale zawsze oparte na źródłach i faktach.

1 KOMENTARZ

  1. Bardzo interesujący artykuł, który w sposób klarowny wyjaśnia różnice między testami unitowymi, integracyjnymi i end-to-end. Podoba mi się szczegółowe omówienie każdego rodzaju testów oraz wskazanie, kiedy należy je stosować. Dzięki temu artykułowi łatwiej jest zrozumieć, jakie testy powinny być wykonywane na poszczególnych etapach procesu CI/CD.

    Jednakże brakuje mi więcej konkretnych przykładów zastosowania testów w praktyce oraz porównania różnych narzędzi do ich automatyzacji. Byłoby to pomocne zwłaszcza dla osób, które dopiero zaczynają swoją przygodę z testowaniem oprogramowania. Mam nadzieję, że w przyszłych artykułach zostaną poruszone te kwestie.

Możliwość dodawania komentarzy nie jest dostępna.