Niestabilny test potrafi zepsuć więcej niż jeden build: raz przechodzi, raz pada, a przy tym samym kodzie daje sprzeczne sygnały. W branży taki przypadek nazywa się flaky test. W praktyce to jeden z najdroższych problemów w automatyzacji, bo podkopuje zaufanie do zestawu testów, spowalnia ciągłą integrację i zmusza zespół do zgadywania, zamiast do podejmowania decyzji. W tym tekście pokazuję, jak go rozpoznać, które metody testowania są najbardziej podatne na flakiness i jak ograniczać problem bez pudrowania objawów.
Najważniejsze fakty o niestabilnych testach
- Niestabilny test to test, który przy tym samym kodzie raz przechodzi, a raz się wywraca.
- Najbardziej narażone są testy szerokie: integracyjne i end-to-end; najmniej podatne są dobrze izolowane testy jednostkowe.
- Najczęstsze źródła problemu to stan współdzielony, czas, kolejność uruchomień, dane testowe, równoległość i infrastruktura.
- Retry i quarantine pomagają operacyjnie, ale nie zastępują naprawy przyczyny.
- Największy efekt daje hermetyczne środowisko, czyste dane i ograniczenie zależności od globalnego stanu.
Czym jest niestabilny test i dlaczego podkopuje zaufanie do zestawu testów
Ja od razu rozdzielam dwa przypadki: test, który ujawnia prawdziwy błąd, i test, który zmienia wynik bez zmiany zachowania systemu. W drugim scenariuszu sam test przestaje być wiarygodnym sygnałem. To oznacza, że czerwony wynik nie mówi już jasno, czy zepsuł się kod, dane, środowisko czy kolejność uruchomień. Jeśli taki stan trwa długo, zespół zaczyna ignorować alarmy, a to jest najgorszy możliwy finał.
Największy koszt nie leży w pojedynczym błędzie, tylko w erozji zaufania. Gdy build bywa czerwony „bez powodu”, każdy kolejny alert traci wagę. Dlatego traktuję niestabilność nie jako kosmetyczny defekt testu, ale jako sygnał, że jakiś ważny element systemu testowego nie jest wystarczająco kontrolowany. To prowadzi wprost do pytania: które metody testowania są na to najbardziej narażone?
Im szerzej test dotyka systemu, tym więcej rzeczy musi zagrać jednocześnie. I właśnie dlatego różne metody testowania trzeba oceniać osobno, zamiast wrzucać je do jednego worka.
Które metody testowania są najbardziej narażone na niestabilność
Dokumentacja pytest zwraca uwagę, że im wyżej jesteś w stosie, tym więcej stanu musisz kontrolować, a to zwiększa ryzyko niestabilnych wyników. Z mojego doświadczenia wynika dokładnie to samo: mały, dobrze odizolowany test zwykle zachowuje się przewidywalnie, a szeroki test przez UI lub sieć częściej łapie przypadkowe zakłócenia.
| Metoda testowania | Podatność na niestabilność | Dlaczego tak się dzieje | Co zwykle pomaga |
|---|---|---|---|
| Testy jednostkowe | Niska | Sprawdzają mały fragment logiki i łatwo izolują zależności. | Kontrola czasu, losowości i globalnego stanu oraz proste fixture’y. |
| Testy komponentowe | Niska do średniej | Łączą kilka klas lub modułów, ale nadal da się je dobrze odseparować. | Fakes, stabilne dane i jasny zakres odpowiedzialności. |
| Testy integracyjne | Średnia do wysokiej | Dotykają bazy danych, kolejki, cache, API lub kilku usług naraz. | Hermetyczne środowisko, czyszczenie danych i jawne kontrakty. |
| Testy end-to-end | Wysoka | Przechodzą przez UI, warstwę sieciową i wiele zależności po drodze. | Ograniczenie zakresu, stabilne selektory i czekanie na warunek, nie na czas. |
| Testy kontraktowe | Średnia | Najczęściej zawodzą, gdy klient i dostawca inaczej interpretują schemat lub kontrakt. | Wersjonowanie kontraktów i pilnowanie zgodności po obu stronach. |
Zestawienie nie mówi, że testy szerokie są złe. Mówi tylko, że potrzebują większej dyscypliny i lepszego środowiska. Jeśli twoja strategia opiera się głównie na testach end-to-end, możesz mieć zielony pipeline i jednocześnie słaby sygnał jakości. Właśnie dlatego warto najpierw rozpoznać, skąd bierze się niestabilność, a dopiero potem ją leczyć.

Skąd biorą się niestabilne wyniki w testach
Google Testing Blog porządkuje źródła flakiness w cztery warstwy: sam test, framework uruchamiający, system pod testem wraz z zależnościami oraz OS i sprzęt. To dobre uproszczenie, bo szybko pokazuje, że problem nie musi siedzieć w jednym asercie. Czasem winna jest kolejność uruchomień, czasem resztki danych po poprzednim teście, a czasem zwykły brak zasobów na maszynie CI.
- Sam test - brak porządnego cleanupu, zbyt ostre asercje, zależność od czasu lub losowości.
- Framework - zła kolejność uruchomień, równoległość bez izolacji i niedoszacowane zasoby.
- Aplikacja lub system pod testem - race conditions, wolne odpowiedzi, memory leaks, oversubscription zasobów.
- OS i sprzęt - niestabilna sieć, dysk, obciążony host lub współdzielona infrastruktura.
Ja zawsze zwracam szczególną uwagę na stan, którego test nie kontroluje wprost. Jeśli test używa lokalnego czasu, strefy czasowej, zegara systemowego albo współdzielonej bazy, to nie jest drobiazg techniczny, tylko pełnoprawna zależność. Tak samo zdradliwe bywają testy zależne od kolejności albo takie, które zostawiają śmieci po sobie i psują następne uruchomienia.
Gdy wiesz już, z której warstwy płynie problem, diagnoza przestaje być zgadywaniem, a staje się sekwencją sprawdzalnych kroków.
Jak diagnozuję problem krok po kroku
Ja zaczynam od odtworzenia błędu w tych samych warunkach, bez poprawiania czegokolwiek po drodze. Jeśli to niemożliwe, nie zgaduję na ślepo, tylko zbieram tropy: commit, środowisko, seed, czas uruchomienia, kolejność testów i obecność równoległości. W praktyce najwięcej czasu oszczędza prosta mapa symptomów.
| Co widzę | Co podejrzewam | Co sprawdzam najpierw |
|---|---|---|
| Test pada tylko przy uruchomieniu równoległym | Współdzielony stan albo zależność od kolejności | Uruchomienie sekwencyjne, unikalne dane, izolowane fixture’y |
| Test pada tylko na CI | Różnica środowiska, limity zasobów, obraz kontenera | Porównanie lokalnie i na CI, limity CPU i RAM, zależności zewnętrzne |
| Test pada o określonej porze lub w konkretnej strefie | Czas systemowy, timezone, logika oparta na dacie | Zamrożenie zegara, jawne ustawienie strefy, brak zależności od „teraz” |
| Test zaczyna się psuć po innym teście | Brak cleanupu albo wyciek danych | Teardown, rollback, czyszczenie zasobów po każdym uruchomieniu |
| Test raz przechodzi, raz nie, bez zmian w kodzie | Losowość, timing, race condition | Seed, logi czasowe, synchronizacja, brak fixed sleep |
Jeśli problem znika po wyłączeniu równoległości albo po nadaniu unikalnych danych testowych, masz już bardzo mocny trop. Jeśli pojawia się tylko na CI, porównuję obrazy kontenerów, limity CPU i RAM oraz zależności zewnętrzne. Taka diagnostyka jest wolniejsza niż kolejny retry, ale prowadzi do naprawy, a nie do maskowania symptomów. Dopiero wtedy ma sens rozmowa o prewencji w kodzie i danych testowych.
Jak ograniczam flakiness w kodzie i danych testowych
Najlepsze efekty daje połączenie izolacji stanu, przewidywalnych fixture’ów i jawnego sterowania tym, co wcześniej było przypadkowe. Ja zwykle zaczynam od tego, co można uprościć bez utraty wartości testu, a dopiero potem ruszam bardziej kosztowne elementy środowiska.
| Słaby wzorzec | Lepsze podejście | Dlaczego działa |
|---|---|---|
sleep(2) przed asercją |
Czekanie na konkretny warunek | Test nie zgaduje, tylko reaguje na realny stan aplikacji. |
| Współdzielona baza lub wspólny katalog | Unikalny namespace, rollback albo pełne czyszczenie po teście | Zmniejsza ryzyko, że jeden test zostawi śmieci dla kolejnego. |
| Losowość bez kontroli | Seed, deterministyczny generator lub stałe dane | Ten sam test daje ten sam wynik, więc da się go odtworzyć. |
| Zbyt szeroki asercyjny „zrzut wszystkiego” | Jedna jasna odpowiedzialność i węższe sprawdzenie | Mniej szumu, mniej fałszywych alarmów i łatwiejsza diagnoza. |
| Bezpośrednia zależność od zewnętrznego API | Fake, stub albo test kontraktowy | Test nie zależy od chwilowej kondycji cudzej usługi. |
Jeśli tylko możesz, buduj środowisko hermetyczne: wszystko, czego test potrzebuje, powinno być w jego zasięgu, a nie po drugiej stronie sieci. To nie znaczy, że nigdy nie wolno korzystać z prawdziwej bazy czy API. Znaczy tylko, że trzeba to robić świadomie i w warstwie, która ma sens kosztowy. Ale sam kod to jeszcze nie wszystko, bo źle ustawiony pipeline potrafi zepsuć nawet dobrze napisane testy.
Jak zarządzać niestabilnymi testami w CI bez psucia jakości
Sama naprawa kodu nie wystarcza, jeśli pipeline dalej traktuje niestabilne testy jak zwykłe błędy. W ciągłej integracji trzeba rozdzielić dwie rzeczy: doraźne utrzymanie przepływu pracy i realne leczenie przyczyny. Inaczej zespół szybko nauczy się, że czerwony wynik można po prostu kliknąć jeszcze raz.
| Działanie | Kiedy ma sens | Czego nie robić |
|---|---|---|
| Pojedynczy retry w CI | Gdy chcesz odróżnić chwilowy szum od stałej awarii | Nie zamieniaj go w stałe lekarstwo na każdy czerwony build. |
| Quarantine z właścicielem i datą wygaśnięcia | Gdy test blokuje releas, ale potrzebujesz czasu na diagnozę | Nie pozwól, by test trafił do wiecznego „później”. |
| Śledzenie flake rate | Gdy chcesz wiedzieć, które obszary naprawdę generują hałas | Nie opieraj rozmowy o jakości wyłącznie na intuicji. |
| Rozdzielenie etapów pipeline’u | Gdy testy szerokie są wolne albo zbyt kruche, by blokować wszystko | Nie rozciągaj całego pipeline’u tylko dlatego, że jedna warstwa jest ciężka. |
Ja bardzo ostrożnie podchodzę do sytuacji, w której test trzeba uruchomić kilka razy, żeby „się uspokoił”. To może być chwilowo użyteczne jako amortyzacja, ale nie jest rozwiązaniem. Jeśli nie ma ownera, daty powrotu do naprawy i metryki, która pokazuje skalę problemu, quarantine staje się po prostu schowkiem na dług techniczny. Jeśli to uporządkujesz, reszta zwykle zaczyna się stabilizować szybciej, niż zespoły się spodziewają.
Co naprawdę stabilizuje testy na dłuższą metę
Największą różnicę robi nie retry, tylko konsekwencja: krótsze i bardziej izolowane testy tam, gdzie się da, oraz wyższe warstwy tylko tam, gdzie naprawdę wnoszą wartość. Jeśli test zależy od czasu, sieci, kolejności albo globalnego stanu, traktuję to jako zaproszenie do jawnego kontrolowania tej zależności, a nie jako drobiazg do zignorowania.
W praktyce wygrywa zespół, który ma trzy rzeczy jednocześnie: czyste dane testowe, przewidywalne środowisko i jasne ownership dla każdego niestabilnego przypadku. Gdy to działa, zielony build znów znaczy to, co powinien: że sygnał jest wiarygodny, a nie tylko chwilowo uciszony. To właśnie wtedy automatyzacja przestaje przeszkadzać i zaczyna naprawdę pomagać.