Automatyzacja testów w CI: co uruchamiać, a co pominąć

0
79
1/5 - (1 vote)

Nawigacja:

Po co w ogóle automatyzować testy w CI

Szybka informacja zwrotna jako fundament DevOps

Automatyzacja testów w CI jest narzędziem do uzyskiwania możliwie szybkiej, wiarygodnej informacji o jakości zmian. Jeśli na wynik testów trzeba czekać godzinami, zespół zaczyna „strzelać na ślepo”: merguje zmiany z nadzieją, że „będzie dobrze”, a błędy wychodzą dopiero na środowiskach wspólnych lub, co gorsza, produkcji. Gdy testy uruchamiają się automatycznie po każdym istotnym zdarzeniu (commit, pull request, release), zespół natychmiast widzi, czy konkretna zmiana zepsuła aplikację.

Kultura DevOps zakłada ciągłe, małe kroki i częste wdrożenia. Taki tryb pracy jest realny tylko wtedy, gdy jakość jest sprawdzana w sposób automatyczny i powtarzalny. Ręczne odpalanie testów zawsze będzie przegrywać z presją czasu, szczególnie przy wielu równoległych gałęziach i kilku zespołach dotykających tego samego repozytorium.

Różnica między „mam testy” a „testy są w pipeline”

Spora część zespołów deklaruje, że „ma testy”, ale w praktyce oznacza to kilka klas testowych, które ktoś od czasu do czasu odpali lokalnie. Taki zestaw daje złudne poczucie bezpieczeństwa, bo nie jest elementem stabilnego procesu. Pojawiają się pytania: kto, kiedy i jak często je uruchamia? Czy wyniki są gdzieś zapisywane? Czy można wrócić do historii awarii?

Moment przełomowy następuje wtedy, gdy testy stają się integralną częścią pipeline’u CI. Oznacza to, że:

  • uruchamiają się automatycznie przy ściśle określonych zdarzeniach (push, PR, tag release itp.),
  • ich wynik wpływa na dalsze kroki (np. blokuje merge lub deployment),
  • są widoczne dla całego zespołu w jednym miejscu (logi CI, dashboard jakości).

Różnica w praktyce jest znacząca: zespół nie dyskutuje już „czy testy były odpalone”, tylko dlaczego pipeline jest czerwony i co trzeba poprawić.

Problemy przy ręcznym odpalaniu testów

Ręczne uruchamianie testów ma tendencję do psucia się dokładnie wtedy, kiedy rośnie presja czasu. Typowe scenariusze:

  • developer spieszy się z wypuszczeniem hotfixa, „na chwilę” pomija pełny zestaw testów, błąd wchodzi w kolejne wydanie,
  • część osób odpala testy tylko na swoim module, ignorując resztę systemu, co przerzuca odpowiedzialność na innych,
  • testy są zależne od lokalnego środowiska, więc część zespołu widzi inne wyniki niż reszta.

Pojawiają się też konflikty: ktoś merge’uje zmiany z zielonymi testami u siebie, ale pipeline na gałęzi głównej dopiero potem pokazuje, że w połączeniu z innymi zmianami coś się rozsypało. Bez zautomatyzowanego, wspólnego punktu weryfikacji trudno wyciągać wnioski i porządkować odpowiedzialność.

Dlaczego bez strategii testowej CI staje się wąskim gardłem

Samo dodanie testów do CI nie rozwiązuje problemu. Jeśli zespół bezrefleksyjnie „wrzuci wszystko” do jednego pipeline’u i każe uruchamiać każdy test na każdy commit, proces szybko stanie się nie do zniesienia. Godzinne pipeline’y, kolejki zadań w narzędziu CI, frustrujące oczekiwanie na zielone światło – to klasyczny obraz CI bez strategii.

Automatyzacja testów w CI musi być przemyślana: które testy uruchamiają się zawsze, które warunkowo, a które poza głównym pipeline’em. Dobrze zaprojektowana strategia testów w CI:

  • utrzymuje czas pipeline’u w rozsądnym przedziale (minuty, nie godziny),
  • zapewnia wysoki poziom zaufania do wyniku (mało fałszywych alarmów),
  • pozwala łatwo rozszerzać zestaw testów wraz ze wzrostem systemu.

Bez takiej strategii każdy kolejny test może tylko pogarszać sytuację, zamiast budować bezpieczeństwo wdrożeń.

Krótkie uporządkowanie: rodzaje testów a ich koszt w CI

Podział funkcjonalny testów a pipeline

Rozsądna automatyzacja testów w CI zaczyna się od nazwania, z czym w ogóle mamy do czynienia. Najczęściej spotykane typy:

  • testy jednostkowe – sprawdzają pojedyncze funkcje, klasy, komponenty bez zewnętrznych zależności; szybkie, tanie, idealne do częstego odpalania,
  • testy integracyjne – sprawdzają współpracę kilku modułów, czasem z użyciem baz danych, kolejek, cache; zwykle wolniejsze i bardziej wrażliwe na środowisko,
  • testy kontraktowe – w mikroserwisach weryfikują, czy interfejsy usług (API) spełniają uzgodnione kontrakty; ważne przy równoległym rozwoju wielu zespołów,
  • testy end-to-end (e2e) – przechodzą przez pełne, krytyczne ścieżki biznesowe jak realny użytkownik lub inny system; często bardzo kosztowne,
  • testy UI – operują na interfejsie użytkownika (przeglądarka, aplikacja mobilna), zwykle wymagają stabilnego środowiska i są podatne na fluktuacje,
  • testy wydajnościowe – sprawdzają czas odpowiedzi, przepustowość, zachowanie przy obciążeniu,
  • testy bezpieczeństwa – od prostych skanów podatności w bibliotekach po głębokie testy penetracyjne,
  • testy smoke – krótki zestaw sprawdzający, czy kluczowe funkcje w ogóle działają,
  • testy regresyjne – szerszy pakiet mający wykryć, czy nowe zmiany nie zepsuły istniejących funkcji.

Nie wszystkie z tych typów nadają się do tego, by odpalać je za każdym razem, gdy pojawia się nowy commit. Niektóre lepiej traktować jako zabezpieczenia okresowe lub uruchamiane tylko przy konkretnych wydarzeniach (np. release candidate).

Podział według kosztu: czas, środowisko, flakiness

Sam rodzaj testu to za mało. Dla strategii CI istotny jest koszt uruchomienia testu. Można go patrzeć przez kilka pryzmatów:

  • czas wykonania – ile realnie trwa uruchomienie danego zestawu testów,
  • koszt środowiska – czy wymagają specjalnych baz danych, kontenerów, zewnętrznych usług,
  • flakiness – jak często te testy zmieniają status z zielonego na czerwony bez zmian w kodzie,
  • koszt utrzymania – jak często trzeba je poprawiać po zmianach w systemie.

Dwa zestawy testów integracyjnych mogą teoretycznie być podobne, ale jeśli jeden trwa 10 sekund, a drugi 20 minut, ich rola w pipeline będzie zupełnie inna. Tak samo testy e2e – pojedynczy szkieletowy smoke e2e jest tani, pełna siatka testów UI z setkami scenariuszy może zablokować pipeline na pół godziny.

Jak mierzyć koszt testów w praktyce

W ocenie kosztu testów przydają się proste, ale konsekwentne metryki:

  • time-to-feedback – czas od pusha do otrzymania wyników krytycznych testów,
  • średni czas trwania grup testów (np. unit, integration, e2e-fast, e2e-full),
  • częstotliwość awarii testu w stosunku do liczby uruchomień (pozwala zidentyfikować flaky testy),
  • procent awarii pipeline’u spowodowany problemami środowiskowymi (a nie błędami w kodzie).

Na tej podstawie da się zdecydować, czy dany zestaw testów ma sens w głównym pipeline’ie, czy powinien zostać przeniesiony do cyklicznego joba nocnego, czy może wymaga refaktoryzacji, aby w ogóle go dalej utrzymywać.

Dlaczego ten podział jest kluczowy przy decyzji „co uruchamiać, a co pominąć”

Strategia testów w CI wymaga bilansowania ryzyka: im więcej testów, tym większe bezpieczeństwo, ale też wyższy koszt czasowy i infrastrukturalny. Podział na rodzaje testów i ich koszt pozwala budować macierz testów i pipeline, czyli:

  • jakie testy uruchamiają się przy każdym commicie,
  • jakie przy każdym pull requeście,
  • jakie tylko dla release’ów lub nocnych buildów.

Bez takiego uporządkowania bardzo łatwo wpaść w skrajności: albo pipeline uruchamia jedynie szybkie testy jednostkowe i przepuszcza krytyczne błędy, albo odpala wszystko naraz i zabija produktywność zespołu.

Zbliżenie na ramiona robota przemysłowego przy automatycznej linii montażowej
Źródło: Pexels | Autor: Ludovic Delot

Kryteria podejmowania decyzji: co wchodzi do CI „zawsze”, a co „warunkowo”

Krytyczność funkcji i ryzyko zmian

Pierwsze kryterium: jak krytyczna jest dana funkcja i jak wysokie ryzyko niosą zmiany w tym obszarze. Funkcje związane z płatnościami, logowaniem użytkowników, integralnością danych powinny być chronione mocniejszym zestawem testów niż mało istotne elementy UI typu animacje czy layout panelu administracyjnego.

Jeśli zmiana dotyka:

  • modelu danych używanego przez wiele modułów,
  • kluczowych API wykorzystywanych przez inne systemy,
  • logiki finansowej lub obszarów podlegających regulacjom,

to warto, aby pipeline wymuszał bogatszy zestaw testów, nawet kosztem dłuższego czasu trwania. Z kolei przy modyfikacji dokumentacji technicznej czy statycznych zasobów frontendu wystarczy uproszczony pakiet kontrolny.

Czas trwania testu i stabilność

Kolejne kryterium to czas wykonania pojedynczego testu lub grupy testów oraz ich stabilność. Testy:

  • krótkie i stabilne – idealne na każdy commit i PR,
  • średnio długie, stabilne – dobre na PR oraz pipeline release’owy,
  • długie lub niestabilne – lepiej przerzucić na osobne pipeline’y odpalane okresowo.

Flaky testy w CI potrafią zniszczyć zaufanie zespołu do automatyzacji. Jeśli co trzeci build jest czerwony z powodu kapryśnych testów, ludzie zaczynają ignorować wyniki, retry’ować pipeline „aż się uda”, a prawdziwe błędy giną w szumie. Dlatego jednym z warunków obowiązkowego quality gate powinny być stabilne testy o przewidywalnym czasie działania.

Wymagania biznesowe i compliance

Niektóre zestawy testów pojawiają się wyłącznie z powodów biznesowych lub regulacyjnych. Przykładowo:

  • klient wymaga potwierdzenia, że przed każdym wydaniem przechodzą określone testy bezpieczeństwa,
  • organizacja ma politykę, że zmiany w obszarze płatności muszą przejść konkretne scenariusze regresyjne,
  • branża regulowana (finanse, medycyna) wymaga artefaktów z konkretnych testów do audytu.

Takie wymagania trzeba świadomie wpiąć w pipeline: albo jako etap w release pipeline (np. pełny skan bezpieczeństwa przy tworzeniu wersji produkcyjnej), albo jako cykliczne joby z artefaktami raportów. Kluczowe jest, by nie mieszać ich z codziennym, szybkim pipeline’em developerskim, który musi pozostać lekki.

Monolit kontra mikroserwisy – różne strategie

Architektura systemu wpływa mocno na strategię uruchamiania testów w CI.

Dla monolitu:

  • większość zmian potencjalnie wpływa na cały system,
  • testy integracyjne mogą być ciężkie, bo dotykają wielu modułów naraz,
  • czas budowy i uruchamiania testów bywa długi, więc selektywność staje się krytyczna.

Dla mikroserwisów:

  • pipeline może być podzielony na poziomie serwisu,
  • dużą rolę odgrywają testy kontraktowe i integracje pomiędzy usługami,
  • część testów e2e dotyczy tylko konkretnego serwisu, a część – całego landscape’u.

W monolicie techniki selektywnego uruchamiania testów po diffie mają szczególnie duże znaczenie. W mikroserwisach pipeline’y usług mogą być lżejsze, natomiast mocno rośnie znaczenie wspólnych testów e2e oraz kontraktowych.

Przykładowe proste reguły dla różnych zdarzeń

Logicznym podejściem jest zdefiniowanie jasnych reguł typu „na każdy commit X, na PR Y, na release Z”. Przykładowa macierz:

Przykładowe macierze dla commitów, PR i release’ów

Prosty, zrozumiały dla całego zespołu podział może wyglądać następująco:

  • commit do feature brancha:
    • lint + formatowanie,
    • testy jednostkowe,
    • najszybsze testy integracyjne (np. z wbudowaną bazą in-memory),
    • opcjonalne testy smoke backendu (jeśli mieszczą się w budżecie czasu).
  • pull/merge request do głównej gałęzi:
    • pełny zestaw unit testów,
    • szerszy zestaw testów integracyjnych,
    • krótkie testy e2e-smoke dla krytycznych ścieżek,
    • statyczna analiza kodu, skan bezpieczeństwa zależności.
  • pipeline release’owy:
    • ponowne uruchomienie testów z poziomu PR (jako sanity check),
    • pełniejsza siatka testów e2e i UI,
    • testy regresyjne dla wskazanych obszarów,
    • testy wydajnościowe i bezpieczeństwa według polityki organizacji.

Taka macierz jest punktem wyjścia. Później można ją dostosowywać na podstawie realnych metryk: gdzie pojawia się najwięcej błędów, co realnie blokuje zespół, które testy niczego nie wykrywają przez długie tygodnie.

Domyślny zestaw testów na każdy commit do feature brancha

Cel: szybka informacja zwrotna dla programisty

Pipeline dla pojedynczego commitu ma jeden nadrzędny cel: jak najszybciej powiedzieć autorowi, czy jego zmiana ma sens techniczny. Chodzi o błędy składni, oczywiste regresje, niespójności typu „zapomniałem dodać plik do repozytorium”.

Z tego powodu na tym etapie dominują krótkie, przewidywalne testy. Typowy „budżet” czasowy to kilka minut. Jeśli commit pipeline trwa dłużej, programista naturalnie zacznie go omijać lub rzadziej commitować, co pogarsza jakość pracy.

Minimalny pakiet techniczny

W praktyce sensowny zestaw „must have” na każdy commit to:

  • lint i formatowanie – wymuszenie stylu kodu i prostych reguł statycznych (np. brak nieużywanych zmiennych, importów),
  • kompilacja / build – podstawowa weryfikacja, że projekt się buduje (czy to twarda kompilacja, czy bundling frontendu),
  • testy jednostkowe – uruchamiane dla całego modułu lub selektywnie dla zmienionych komponentów,
  • najkrótsze testy integracyjne – tylko jeśli mieszczą się w wyznaczonym budżecie czasu (np. <1–2 minuty).

W wielu zespołach sensowne jest też dodanie szybkiego skanu bezpieczeństwa zależności, który praktycznie nie zwiększa czasu pipeline’u (np. bo opiera się na cache’owanych bazach podatności).

Techniki przyspieszania testów dla commitów

Żeby commit pipeline pozostał lekki, przydają się konkretne techniki optymalizacyjne:

  • równoległe uruchamianie testów – podział pakietu unit testów na kilka jobów,
  • cache zależności i artefaktów – brak ponownego pobierania bibliotek i przebudowy przy każdym commicie,
  • uruchamianie tylko testów powiązanych ze zmianą – np. na bazie mapowania plik → zestaw testów (więcej w sekcji o strategii selektywnej),
  • wydzielenie wolnych kroków do osobnych jobów asynchronicznych (np. cięższe analizy bezpieczeństwa).

Przykład z praktyki: w jednym z zespołów commit pipeline trwał około 18 minut, głównie przez rozbudowane testy integracyjne. Po rozdzieleniu ich na: fast-integration (na commit) i full-integration (na PR) czas spadł do 4–5 minut, a liczba commitów na dewelopera dziennie wzrosła, co od razu poprawiło ciągłość integracji.

Jak traktować błędy na poziomie commitu

Reguła, która dobrze się sprawdza: commit pipeline może być mniej restrykcyjny niż ten na PR, ale nie może być ignorowany. Oznacza to np.:

  • wymóg zielonego commitu przed wysłaniem PR,
  • możliwość lokalnego „skippingu” części testów w wyjątkowych sytuacjach (np. eksperyment), ale z jasną odpowiedzialnością autora,
  • raportowanie statystyk „czerwonych commitów” jako sygnału do poprawy jakości testów lub praktyk programistycznych.

Jeśli commit pipeline jest permanentnie czerwony z powodów niezależnych od zmian (flaky testy, problemy środowiskowe), staje się niewiarygodny i w praktyce przestaje być używany jako narzędzie kontroli jakości.

Co uruchamiać na pull/merge request: brama jakości dla zespołu

Pull request jako wspólna odpowiedzialność

Na poziomie PR następuje przejście z „indywidualnej pracy developera” do „wspólnego kodu zespołu”. Pipeline PR staje się więc bramą jakości, którą musi przejść każda zmiana zanim trafi do gałęzi głównej.

Ten etap jest najlepszym miejscem, by zbalansować szybkość z szerszym zakresem testów. Budżet czasowy może być większy niż na commicie (np. 10–20 minut), pod warunkiem, że zespół uznaje to za akceptowalne opóźnienie w procesie review.

Elementy typowego pipeline’u dla PR

Najczęściej zestaw wygląda rozsądnie, jeśli zawiera:

  • pełne testy jednostkowe – także te, które na commit były odfiltrowane jako „niezwiązane ze zmianą”,
  • szersze testy integracyjne – z prawdziwą bazą danych (np. w kontenerze), integracją z usługami pomocniczymi (kolejki, cache),
  • krótkie testy e2e smoke – dla najważniejszych scenariuszy biznesowych, np. logowanie, ścieżka zakupowa, zapis krytycznego formularza,
  • statyczna analiza kodu (SAST) – na poziomie reguł bezpieczeństwa i jakości (np. SonarQube, linters bezpieczeństwa),
  • skan podatności w zależnościach (SCA) – aby wyłapać niebezpieczne biblioteki przy każdej większej zmianie.

Celem nie jest pełne przetestowanie wszystkiego, ale zminimalizowanie ryzyka wprowadzenia do gałęzi głównej krytycznych regresji lub oczywistych podatności.

Quality gates: co musi być zielone, aby PR został scalony

Warto jasno zdefiniować, które elementy pipeline’u są blokujące, a które mają charakter informacyjny. Mogą to być reguły typu:

  • wszystkie unit i integration tests muszą przejść na zielono,
  • wszystkie smoke e2e muszą być zielone, z wyjątkiem znanych, udokumentowanych przypadków (np. feature flag),
  • brak nowych krytycznych i wysokich podatności w SAST/SCA (lub zarejestrowane wyjątki z uzasadnieniem),
  • brak nowych ostrzeżeń typu „code smell” powyżej uzgodnionego progu.

Elementy nieblokujące (np. pewne metryki jakościowe, długi raport pokrycia kodu) mogą być jedynie sygnałem do dyskusji w code review, ale nie powinny zatrzymywać scalania, jeśli nie ma ku temu wyraźnych powodów.

Jak łączyć review ludzkie z automatycznym

Automatyczne testy na PR nie zastępują przeglądu kodu. Dobrze, jeśli działają komplementarnie:

  • pipeline automatyczny pilnuje regresji technicznych i reguł organizacyjnych (np. brak podatności, styl kodu),
  • reviewerzy skupiają się na logice biznesowej, czytelności, architekturze.

W praktyce przydatna jest zasada: „review bez zielonego pipeline’u jest wyjątkiem, a nie normą”. Chroni to zespół przed analizowaniem kodu, który i tak nie przejdzie testów.

Specjalne zasady dla dużych refaktoryzacji

Przy dużych refaktoryzacjach sensowne jest tymczasowe zaostrzenie wymagań na poziomie PR. Może to oznaczać:

  • uruchamianie szerszego zestawu testów e2e tylko dla PR oznaczonych etykietą refactor/critical,
  • wymaganie dwóch reviewerów, jeśli pipeline pokazuje dużą liczbę dotkniętych plików,
  • tymczasowe rozszerzenie zakresu testów regresyjnych w obszarze, którego dotyczy refaktoryzacja.

Dzięki temu duże zmiany przechodzą przez ciaśniejszą „bramkę”, ale bez obciążania w ten sam sposób wszystkich codziennych, drobnych PR-ów.

Programista pracujący nad kodem na laptopie i monitorze w biurze
Źródło: Pexels | Autor: Jakub Zerdzicki

Co odpalać przed release: pełna siatka zabezpieczeń

Release pipeline jako ostatni filtr ryzyka

Pipeline release’owy (np. na gałęzi release/* lub przy tagowaniu wersji) jest dobrym miejscem na testy ciężkie, ale rzadko potrzebne. Jego zadaniem jest zminimalizowanie ryzyka wdrożenia wersji, w której różne „niskie” błędy zsumowały się w poważny problem.

Czas takiego pipeline’u może być większy, często liczony w dziesiątkach minut, a wyjątkowo w godzinach – pod warunkiem, że jest to świadoma decyzja i wpasowuje się w rytm releasów (np. raz na tydzień).

Rozszerzony zestaw testów funkcjonalnych

Na tym etapie uruchamia się zwykle:

  • pełny pakiet testów e2e – pokrywający nie tylko krytyczne ścieżki, ale też mniej typowe scenariusze,
  • szersze testy UI – także takie, które są zbyt wrażliwe lub zbyt wolne na codzienny PR,
  • pełne testy regresyjne dla głównych modułów (np. płatności, raporty, integracje z partnerami),
  • testy kontraktowe dla mikroserwisów – w obu kierunkach (provider i consumer).

Jeśli system ma zewnętrzne integracje, to release pipeline jest dobrym momentem na uruchomienie scenariuszy end-to-end z udziałem sandboxów partnerów, o ile istnieją i da się je skonfigurować automatycznie.

Testy wydajnościowe i bezpieczeństwa w kontekście releasu

Dla wielu zespołów pełne testy wydajnościowe i bezpieczeństwa nie są uruchamiane przy każdym release, lecz:

  • cyklicznie (np. co n-ty release lub raz na sprint),
  • warunkowo (np. przy istotnych zmianach w obszarach wrażliwych na wydajność lub bezpieczeństwo).

Możliwy układ:

  • light performance tests:
    • na każdym release – krótki smoke performance (np. kilka scenariuszy z umiarkowanym obciążeniem),
  • full performance tests:
    • np. raz na kwartał lub przy dużej zmianie architektonicznej – dłuższe testy obciążeniowe, stress i soak.
  • skany bezpieczeństwa:
    • SCA/SAST – na każdym PR i release,
    • DAST / dynamiczne testy bezpieczeństwa – na release candidate lub okresowo na środowisku staging,
    • ręczne testy penetracyjne – według cyklu organizacji (np. roczne, półroczne).

Kluczowe jest, aby release pipeline generował artefakty raportów (wydajność, bezpieczeństwo, pokrycie testami), które można później pokazać klientowi, audytorowi czy zespołowi SRE.

Symulacja migracji i operacji na danych

Jeśli wydania zawierają migracje bazy danych, to release pipeline powinien obejmować:

  • testy migracji na dużym wolumenie danych (przynajmniej zbliżonym do produkcyjnego),
  • testy rollbacku, jeśli architektura systemu i polityka organizacji to dopuszcza,
  • weryfikację integralności danych po migracji (np. konsystencja kluczy, brak „sierot”).

Błędy w migracjach są często kosztowne do naprawienia. Dlatego lepiej poświęcić dodatkowe kilkanaście minut na pipeline release’owy niż później walczyć z odzyskiwaniem danych na produkcji.

Strategia selektywnego uruchamiania testów: impact-based i testy zależne od zmienionego kodu

Dlaczego selektywność jest konieczna w dużych projektach

W rozbudowanych systemach pełne uruchamianie wszystkich testów przy każdym commicie czy PR szybko staje się niewykonalne. Pakiet testów rośnie, a wraz z nim czas pipeline’u. Rozwiązaniem jest impact-based testing, czyli uruchamianie przede wszystkim tych testów, które mają szansę zostać dotknięte przez zmianę.

Jak technicznie wybierać testy zależne od zmiany

Selektor testów można zbudować na kilku poziomach złożoności. Najprostsze podejścia działają na poziomie plików i katalogów, bardziej zaawansowane – na grafie zależności i historii pokrycia.

Praktyczne warianty:

  • mapowanie katalogów na zestawy testów – np. zmiana w billing/* odpala wszystkie testy z tests/billing/*,
  • tagowanie testów według modułów – testy oznaczone np. @billing, @search; pipeline wybiera tagi na podstawie zmienionych plików,
  • analiza importów/dependencji – na podstawie grafu zależności modułów da się wskazać, które testy potencjalnie dotyka zmiana,
  • wykorzystanie danych z pokrycia – uruchamianie testów, które w poprzednich przebiegach dotykały zmodyfikowanych linii/plików.

Dwa ostatnie podejścia są bardziej precyzyjne, ale wymagają dodatkowej infrastruktury (np. okresowego generowania grafu zależności, trzymania historii pokrycia w artefaktach).

Łączenie podejść: safety net i priorytety

Skuteczny system selekcji zwykle łączy kilka mechanizmów. Typowy wzorzec:

  • zawsze uruchamiany jest mały zestaw „globalnych” testów smoke (unit/integration),
  • warunkowo odpalane są testy zależne od zmiany, wyszukane po tagach, ścieżkach i pokryciu,
  • okresowo (np. raz dziennie na gałęzi głównej) uruchamiany jest pełny pakiet, żeby złapać ewentualne luki w selekcji.

Takie „safety nety” ograniczają ryzyko, że pojedynczy błąd w algorytmie selektora spowoduje długie, niezauważone dziury w testowaniu.

Selekcja testów przy monorepo i wielu komponentach

W monorepo problem doboru testów jest szczególnie istotny, bo zmiana w jednym komponencie nie powinna wywoływać lawiny testów dla wszystkich pozostałych. Dobrą praktyką jest:

  • zdefiniowanie jawnych granic komponentów (np. w pliku konfiguracyjnym repozytorium),
  • przypisanie dedykowanych jobów CI do każdego komponentu,
  • powiązanie jobów z ścieżkami plików – jeśli zmiana dotyka tylko katalogów A i B, uruchamiane są wyłącznie joby A i B, plus wspólne testy cross-component,
  • udokumentowanie testów „wspólnych”, które muszą być odpalane, gdy zmienia się warstwa infrastrukturalna (np. shared library, CI templates).

Przy takim podejściu łatwiej utrzymać czytelność pipeline’u i zrozumieć, dlaczego akurat te testy ruszyły przy danej zmianie.

Metryki, które pomagają zweryfikować skuteczność selekcji

Jeśli selektywność ma być czymś więcej niż zbiorem heurystyk, trzeba ją mierzyć. Przydatne są szczególnie:

  • czas trwania pipeline’u przed i po wdrożeniu selekcji (czy faktycznie jest szybciej),
  • odsetek błędów wykrytych na późniejszych etapach (PR/release) vs. commit – czy nie rośnie dramatycznie po wprowadzeniu selekcji,
  • „flaky gap” – czy selekcja nie maskuje niestabilnych testów, które wcześniej losowo „wybuchały” na wczesnych etapach,
  • pokrycie kodu w obszarach często zmienianych – czy ciągle utrzymuje akceptowalny poziom.

Jeśli po wdrożeniu selekcji pipeline skraca się o połowę, ale liczba regresji wykrywanych dopiero na release rośnie kilkukrotnie, to znaczy, że algorytm jest zbyt agresywny lub safety net zbyt mały.

Jak wprowadzać selekcję testów bez paraliżu zespołu

Przejście z modelu „odpalamy wszystko” na model „odpalamy to, co trzeba” opłaca się wprowadzać iteracyjnie. Sprawdza się sekwencja:

  1. Faza obserwacyjna – zbieranie danych o tym, które testy najczęściej łapią błędy, jak długo trwają, które są powiązane z jakimi modułami.
  2. Faza cieniowania (shadow mode) – selektor decyduje, które testy powinny się uruchomić, ale pipeline wciąż odpala pełny zestaw; wyniki selektora zapisuje się w logach/raportach.
  3. Stopniowe odcinanie – najpierw zmniejsza się zestaw testów na commicie, później na PR-ach, pozostawiając pełny pakiet na nocnym buildzie lub release.
  4. Regularna korekta reguł – przegląd raz na kilka sprintów; dodawanie nowych mapowań i tagów w miarę rozwoju systemu.

Taki tryb redukuje opór zespołu („boimy się, że coś przestanie się odpalać”) i pozwala złapać błędy w logice selekcji zanim zaczną wpływać na jakość releasów.

Selektywność a niestabilne testy

Impact-based testing nie rozwiązuje problemu niestabilnych testów, ale często go uwidacznia. Jeśli pojedynczy test e2e:

  • jest niestabilny,
  • a jednocześnie należy do kluczowego modułu,
  • jest odpalany często przez selektor, bo moduł bywa intensywnie rozwijany,

to pipeline zaczyna blokować zespół częściej niż wcześniej. W takiej sytuacji trzeba podjąć decyzję: albo naprawiamy/odchudzamy test, albo przenosimy go do rzadziej odpalanego etapu (np. nightly), w zamian wzmacniając pokrycie niższymi poziomami testów.

Co świadomie pominąć, opóźnić lub przenieść poza główny pipeline

Rozróżnienie między „must have” a „nice to have”

Nie każdy rodzaj weryfikacji jakości musi znaleźć się w głównym, blokującym pipeline’ie. Kluczowe jest ustalenie, które kontrole:

  • krytyczne biznesowo lub bezpieczeństwowo – bez nich nie wypuszczamy kodu,
  • istotne, ale nieblokujące – mogą dawać sygnały ostrzegawcze,
  • opcjonalne lub eksploracyjne – dobre do uruchamiania cyklicznie albo „na żądanie”.

Taki podział powinien być spisany i znany zespołowi, w przeciwnym razie pipeline z czasem „puchnie” o kolejne dobre pomysły, z których żaden nie został skonfrontowany z budżetem czasowym.

Ciężkie analizy statyczne i metryki jakościowe

Głębokie analizy kodu (np. pełne reguły Sonara, zaawansowane skanery bezpieczeństwa, liczenie złożoności, detekcja duplikatów) potrafią trwać długo. Zamiast ładować wszystko do głównego pipeline’u:

  • podziel reguły na klasy – krytyczne (blokujące PR), ostrzegawcze (raportowane, ale nieblokujące), informacyjne (np. uruchamiane nocą),
  • uruchamiaj pełne analizy na gałęzi głównej lub release, a na PR wykorzystuj lżejszy profil,
  • udostępniaj dashboardy – devy widzą trend jakości, ale nie czekają na zakończenie każdej analizy przy każdym commicie.

Częsta praktyka: szybki linter i podstawowe reguły SAST na każdym PR, a rozbudowany profil Sonara – raz dziennie lub na release candidate.

Długotrwałe testy niefunkcjonalne

Testy wydajnościowe typu stress/soak, długie scenariusze UX (automatyczne klikanie przez setki ekranów) czy testy odporności na awarie (chaos engineering) rzadko nadają się na blokujący krok w codziennym CI. Raczej:

  • tworzy się osobne pipeline’y (np. perf-nightly, chaos-weekly),
  • wiąże się je z konkretnymi tagami (np. v1.4.0-rc1), żeby wyniki było łatwo powiązać z wersją,
  • wymaga się „zielonego” stanu przed określonym typem releasu (np. release major bez czerwonego perf-nightly jest zabroniony).

Zespół produktowy ma wtedy jasność: codzienny development idzie swoim tempem, ale przed dużym wydaniem trzeba przejść stricte zdefiniowaną ścieżkę dodatkowych testów.

Eksploracyjne scenariusze automatyczne i eksperymenty

Automatyzacja sprzyja eksperymentom: testy generowane losowo, fuzzing API, narzędzia crawlujące UI w poszukiwaniu błędów. To cenne źródła defektów, ale niekoniecznie nadają się na stały element głównej ścieżki:

  • często zwielokrotniają czas pipeline’u,
  • potrafią generować dużo szumu (fałszywe alarmy, mało powtarzalne błędy),
  • zwykle opracowuje się je iteracyjnie, a nie jako stałe, przewidywalne testy regresyjne.

Lepszym miejscem na nie są pipeline’y „eksploracyjne”, uruchamiane okresowo lub manualnie przez QA/DevOps, z raportem w formie backlogu potencjalnych problemów do analizy.

Testy „marketingowe” a realna wartość dla CI

Zdarzają się testy, które dobrze wyglądają w raportach („mamy 95% pokrycia UI testami wizualnymi”), ale ich realny wpływ na jakość jest ograniczony. Jeśli:

  • testy często łamią się przy drobnych, nieistotnych zmianach,
  • wymagają ciągłej, czasochłonnej pielęgnacji,
  • rzadko łapią istotne regresje,

to można rozważyć przesunięcie ich do sekcji raportowej (np. uruchamianej raz dziennie) lub zredukowanie ich zakresu do kluczowych scenariuszy. Główny pipeline powinien zawierać przede wszystkim te testy, które przynoszą najwięcej wartości na minutę wykonywania.

Manualne kroki a automatyczny CI

Nie wszystkie kontrole da się zautomatyzować sensownie i opłacalnie. Przykłady:

  • akceptacja UX dla dużych zmian wizualnych,
  • przegląd zgodności z brand bookiem lub wytycznymi prawnymi,
  • złożone scenariusze biznesowe z udziałem wielu organizacji.

Zamiast próbować je na siłę wcisnąć w CI, lepiej:

  • zdefiniować checklisty manualne,
  • powiązać je z kamieniami milowymi releasu (np. „przed oznaczeniem RC musi zostać odhaczona lista UX/legal”),
  • zapisywać decyzje w systemie ticketowym; pipeline może jedynie sprawdzać obecność odpowiednich adnotacji (np. komentarz w Jirze, etykieta „UX-approved”).

CI wtedy pilnuje, że proces został wykonany, ale nie próbuje automatyzować samej decyzji tam, gdzie ma ona charakter głównie ekspercki.

Mechanizmy „escape hatch”: kiedy wolno ominąć testy

Czasem potrzebne jest świadome ominięcie części testów (np. pilny hotfix na produkcję, awaria infrastruktury testowej). Żeby taka możliwość nie zniszczyła całej dyscypliny, musi być otoczona kontrolami:

  • jasne kryteria użycia – np. tylko dla hotfixów oznaczonych konkretną etykietą i tylko na dedykowanej gałęzi,
  • obowiązkowy „follow-up” – pipeline, który odpala pełny zestaw testów już po wdrożeniu hotfixu na produkcję lub staging,
  • rejestr wyjątków – lista PR-ów/releasów, które przeszły z pominięciem standardowych bramek, do okresowego przeglądu.

Dobrze, jeśli taki „escape hatch” jest świadomy i rzadki; jeżeli z czasem staje się normą, to sygnał, że cały model CI jest zbyt restrykcyjny lub źle dopasowany do rytmu biznesu.

Najczęściej zadawane pytania (FAQ)

Po co automatyzować testy w CI, skoro mogę je uruchamiać ręcznie?

Ręczne odpalanie testów działa tylko przy małych projektach i niskim tempie zmian. W momencie, gdy pojawia się kilka gałęzi, kilku developerów i presja czasu, testy odpalane „jak się przypomni” przestają pełnić funkcję zabezpieczenia. Zmiany lądują w mainie lub na środowisku wspólnym bez realnej weryfikacji jakości.

Automatyzacja w CI daje stały, powtarzalny punkt kontroli po każdym pushu, pull requeście czy tagu release. Wynik testów jest widoczny dla całego zespołu, a pipeline może zablokować merge lub deployment, jeśli coś się psuje. Zamiast pytać „czy ktoś odpalił testy?”, zespół widzi od razu: pipeline jest czerwony lub zielony i wie, co dalej robić.

Jakie testy powinny uruchamiać się przy każdym commicie w CI?

Przy każdym commicie opłaca się uruchamiać przede wszystkim testy szybkie i tanie: testy jednostkowe oraz niewielki, dobrze dobrany zestaw testów integracyjnych lub smoke. Chodzi o to, by time-to-feedback liczony od pusha do pierwszego wyniku mieścił się w minutach, a nie w dziesiątkach minut.

Dobrym wzorcem jest: pełny pakiet testów jednostkowych + mały zestaw integracyjnych/e2e-smoke, który sprawdza, czy aplikacja w ogóle „wstaje” i działa kluczowa ścieżka (np. logowanie, prosty zakup). Cięższe scenariusze integracyjne, pełne e2e czy testy wydajnościowe lepiej przenieść na etap PR, buildów nocnych lub pipeline’ów release’owych.

Jak zdecydować, które testy uruchamiać w CI zawsze, a które tylko warunkowo?

Decyzja zależy od dwóch rzeczy: krytyczności funkcji, które test sprawdza, oraz kosztu uruchomienia testu. Jeśli dany zestaw jest szybki, stabilny i dotyka obszarów o wysokim ryzyku (np. płatności, logowanie, integracje między kluczowymi mikroserwisami) – powinien być włączony „zawsze”. Jeśli jest wolny, kruchy środowiskowo i dotyczy mniej krytycznych ścieżek, lepiej uruchamiać go rzadziej.

W praktyce warto stworzyć prostą macierz: testy „zawsze” (commit/PR), testy „warunkowe” (np. tylko dla wybranych gałęzi, tylko przy tagu release, po zmianie konkretnego modułu) oraz testy „okresowe” (nocne buildy, testy przed produkcją). Dzięki temu pipeline główny pozostaje szybki, a jednocześnie system jest dobrze zabezpieczony przed regresjami.

Jak mierzyć koszt testów w CI, żeby nie zabić pipeline’u?

Koszt testów to nie tylko czas ich trwania. Trzeba patrzeć na kilka metryk naraz: średni czas trwania grup testów (unit, integration, e2e-fast, e2e-full), częstotliwość awarii testu względem liczby uruchomień (flakiness) oraz procent awarii pipeline’u wynikający z problemów środowiskowych, a nie realnych bugów w kodzie.

Dobrym podejściem jest tagowanie lub grupowanie testów i regularne przeglądanie ich statystyk. Jeśli jakiś pakiet testów trwa długo i często się „wykłada” z powodów niezwiązanych z kodem, to sygnał, by: skrócić go, ustabilizować lub przenieść poza główny pipeline (np. do jobów nocnych). Celem jest utrzymanie rozsądnego time-to-feedback, szczególnie dla developerów pracujących na co dzień.

Czym różni się „posiadanie testów” od włączenia testów do pipeline’u CI?

„Mam testy” często oznacza kilka klas testowych uruchamianych od czasu do czasu lokalnie. Nie ma wtedy pewności, kto i kiedy je odpala, nie ma historii awarii ani spójnego punktu odniesienia dla całego zespołu. Takie testy dają złudne poczucie bezpieczeństwa – istnieją w repozytorium, ale nie są częścią realnego procesu.

Testy w pipeline’ie CI uruchamiają się automatycznie przy zdefiniowanych zdarzeniach (push, PR, release), ich wynik wpływa na decyzje (np. blokuje merge) i jest widoczny w jednym miejscu (dashboard, logi CI). Różnica jest praktyczna: zamiast lokalnych „eksperymentów” poszczególnych osób mamy wspólną, powtarzalną kontrolę jakości dla całego zespołu.

Dlaczego uruchamianie wszystkich testów na każdy commit spowalnia DevOps?

Jeśli każdy commit uruchamia pełny pakiet testów integracyjnych, e2e, UI, wydajnościowych i bezpieczeństwa, pipeline bardzo szybko zaczyna trwać kilkadziesiąt minut lub dłużej. Pojawiają się kolejki jobów, developerzy czekają na wynik starego pipeline’u, zanim zrobią kolejny push, a tempo pracy spada – mimo że oficjalnie „mamy dużo testów”.

DevOps opiera się na szybkiej informacji zwrotnej i krótkich cyklach zmian. Żeby je utrzymać, trzeba świadomie ograniczać to, co ląduje w głównym pipeline’ie, i przenosić cięższe testy na inne rytmy (np. PR, noc, pre-release). Bez takiej strategii CI staje się wąskim gardłem, a nie wsparciem dla częstych wdrożeń.

Jak uniknąć problemów z flaky testami w CI?

Flaky testy to takie, które raz są zielone, a raz czerwone bez zmian w kodzie. W CI szybko zjadają zaufanie do wyników i powodują, że zespół zaczyna „przeklikiwać się” przez czerwone pipeline’y, traktując je jak szum. Pierwszym krokiem jest mierzenie flakiness – np. zliczanie, ile razy test padł względem liczby jego uruchomień.

Dalsze działania to: izolowanie testów od niestabilnych zależności (mocki zamiast zewnętrznych usług), skracanie i upraszczanie scenariuszy e2e/UI, stabilizacja środowiska CI (kontenery, powtarzalne seedy danych), a w skrajnych przypadkach – wyłączenie szczególnie złych testów z głównego pipeline’u do momentu ich naprawy. Lepszy mniejszy, stabilny zestaw niż rozbudowany, ale nieprzewidywalny.