Regresja — to błąd, który pojawia się po wprowadzeniu zmian w kodzie, mimo że wcześniej ta sama funkcjonalność działała poprawnie. Regresja oznacza, że nowa zmiana „zepsuła“ to, co zostało już napisane i przetestowane wcześniej. To jeden z najczęstszych i najniebezpieczniejszych problemów w rozwoju: naprawiając jeden błąd, programista może nieświadomie zepsuć trzy inne funkcje. Według Capers Jones Software Engineering 2023, średnia gęstość błędów regresyjnych wynosi 1–3 na każde 100 zmienionych linii kodu. Omawiamy przyczyny regresji, metody ich wykrywania i strategie zapobiegania.
Najważniejsze
Regresja — to sytuacja, gdy funkcjonalność, która działała w poprzedniej wersji, przestaje działać po wprowadzeniu zmian. Zmiana może być dowolna: naprawa błędu, dodanie nowej funkcji, refaktoryzacja, aktualizacja biblioteki, a nawet zmiana konfiguracji. Regresja to główny wróg stabilności: każda zmiana ryzykuje zepsucie czegoś, co zostało już sprawdzone i wydane.
Termin pochodzi z testowania: testowanie regresyjne — to ponowne uruchamianie istniejących testów po każdej zmianie. Jeśli wcześniej przechodzący test upada — oznacza to regresję. W szerszym znaczeniu regresja to nie tylko upadek testu, ale także każde pogorszenie zachowania zauważone przez użytkownika lub QA. Według Tricentis State of Testing 2023, regresje stanowią 35–45% wszystkich błędów znajdowanych w produkcji.
Od zwykłego błędu regresja różni się kontekstem czasowym: błąd mógł istnieć zawsze, a regresja jest zawsze wynikiem zmiany. To ważna różnica, ponieważ poszukiwanie przyczyny regresji zaczyna się od analizy zmian: co zostało zmienione między „działało“ a „przestało działać“. Git bisect — standardowe narzędzie do znajdowania commita, który spowodował regresję.
Regresja lokalna — zmiana w module A psuje funkcjonalność w tym samym module A. Przykład: programista przepisuje funkcję sortowania, a ona przestaje poprawnie obsługiwać pustą tablicę. Regresja lokalna jest najłatwiejsza do wykrycia i naprawienia, ponieważ przyczyna i skutek są blisko siebie.
Regresja zdalna — zmiana w module A psuje funkcjonalność w module B, który nie jest bezpośrednio powiązany kodem, ale jest powiązany danymi lub czasem. Przykład: zmiana schematu bazy danych w module „Użytkownicy“ psuje raport w module „Analityka“, który korzysta z tej samej tabeli. Regresje zdalne są najbardziej podstępne: programista nie podejrzewa, że jego zmiana wpłynie na inny moduł.
Regresja efektów ubocznych — zmiana efektu ubocznego (logowanie, buforowanie, wysyłanie powiadomień) psuje oczekiwane zachowanie. Przykład: programista dodał buforowanie w celu przyspieszenia, ale z powodu nieaktualnego cache użytkownicy widzą nieaktualne dane. Regresje efektów ubocznych są trudne do wykrycia przez automatyczne testy, ponieważ efekty uboczne często nie są pokryte testami.
Regresja wydajnościowa — kod nadal działa poprawnie funkcjonalnie, ale wolniej niż wcześniej. Przykład: nowy algorytm szyfrowania daje te same wyniki, ale czas wykonania wzrósł z 2 ms do 200 ms. Regresje wydajnościowe nie są wykrywane przez zwykłe testy jednostkowe — potrzebne są benchmarki i profilowanie.
| Typ regresji | Przykład | Sposób wykrycia |
|---|---|---|
| Lokalna | Zepsute sortowanie | Testy jednostkowe |
| Zdalna | Zmiana schematu BD | Testy integracyjne |
| Efektów ubocznych | Nieaktualny cache | Testy E2E |
| Wydajnościowa | Spowolnienie odpowiedzi | Benchmarki |
Pierwsza przyczyna — powiązanie kodu (coupling). Im silniej moduły zależą od siebie, tym większe prawdopodobieństwo, że zmiana w jednym spowoduje regresję w drugim. Klasyczne antywzorce: God Object (obiekt, który robi wszystko), Shotgun Surgery (zmiana jednego wymaga poprawek w dziesiątkach miejsc), Circular Dependency. Zmniejszenie coupling — zadanie architektury: zasady SOLID, Dependency Injection, architektura heksagonalna.
Druga przyczyna — brak testów dla zmienianej funkcjonalności. Jeśli kod nie jest pokryty testami, programista dowiaduje się o regresji dopiero od QA lub użytkowników. Według Google Testing Blog, projekty z pokryciem testów >75% mają 5 razy mniej regresji niż projekty z pokryciem <25%. TDD (Test-Driven Development) gwarantuje, że testy są napisane przed kodem, a nie „kiedy będzie czas“.
Trzecia przyczyna — czynnik ludzki. Programista nie wie o istnieniu sąsiedniej funkcjonalności, nie rozumie wszystkich zależności lub po prostu się spieszy. Przyczyna — niewystarczające dzielenie się wiedzą o bazie kodu. Rozwiązania: code review z udziałem programistów z innych modułów, pair programming, dokumentacja architektury. Bus factor projektu jest odwrotnie proporcjonalny do liczby udokumentowanych decyzji architektonicznych.
Testowanie regresyjne — to proces ponownego uruchamiania istniejących testów po każdej zmianie w celu sprawdzenia, czy stara funkcjonalność nie została zepsuta. To jedyny sposób, aby zagwarantować, że nowa zmiana nie naruszyła działania istniejącego kodu. Bez testowania regresyjnego każdy release to loteria: programista ma nadzieję, że niczego nie zepsuł, ale nie może tego potwierdzić.
Ręczne testowanie regresyjne — najdroższe i najbardziej nieefektywne podejście. W miarę wzrostu projektu liczba regresyjnych scenariuszy testowych rośnie liniowo, a czas na ich ręczne uruchomienie — wykładniczo. Po 2–3 latach rozwoju ręczne uruchomienie regresji może zajmować 2–3 tygodnie, co uniemożliwia częste wydania. Jedynym wyjściem jest automatyzacja.
Zautomatyzowane testowanie regresyjne dzieli się na poziomy według piramidy testowania:
Według Google Testing Blog, optymalny stosunek: 70% testów jednostkowych, 20% integracyjnych, 10% E2E. Odchylenie od tej proporcji obniża skuteczność testowania regresyjnego: nadmiar testów E2E spowalnia pipeline, niedobór testów jednostkowych pozostawia mikro-błędy niezauważone.
Pierwsza strategia — Full Regression. Uruchamiane są wszystkie testy projektu. Najbardziej niezawodne, ale i najwolniejsze podejście. Stosowane w małych projektach (do 10 000 testów, czas uruchomienia <30 minut). W dużych projektach pełna regresja może zajmować godziny, co czyni CI/CD pipeline niepraktycznym.
Druga strategia — Selective Regression. Uruchamiane są tylko testy związane ze zmienionym kodem. Do określenia powiązań używany jest graf zależności kodu. Narzędzia: Bazel (Google), Nx (JavaScript), sbt (Scala). Selective regression oszczędza 60–80% czasu uruchomienia, ale wymaga dokładnego zbudowania grafu zależności — błędy prowadzą do pominiętych regresji.
Trzecia strategia — Prioritized Regression. Wszystkie testy są rankowane według priorytetu: critical path (najważniejsze scenariusze użytkownika), high risk (kod z historią błędów), changed code (kod objęty zmianą). Najpierw uruchamiane są najbardziej priorytetowe testy — jeśli przechodzą, programista otrzymuje szybką informację zwrotną. Uruchomienie z limitem czasu: w ciągu 10 minut sprawdzane są testy krytyczne, pozostałe — w tle.
Pierwszy i najważniejszy krok — kultura pisania testów. Każda zmiana powinna być acompaniowana testem, który sprawdza, że zmiana działa, oraz testem, który sprawdza, że nic się nie zepsuło. TDD (Test-Driven Development) daje najlepsze rezultaty: programista najpierw pisze upadający test, potem — kod, który go przechodzi. To gwarantuje, że test istnieje przed kodem.
Drugi krok — CI/CD pipeline z obowiązkowym uruchomieniem testów. Pull request nie może być scalony, dopóki wszystkie testy nie przejdą. Nie można „pominąć“ testów z powodu pilności — pilne zmiany przechodzą przyspieszony, ale obowiązkowy zestaw testów. Według Google DevOps Research, zespoły z obowiązkowym CI/CD mają 3 razy mniej regresji w produkcji.
Trzeci krok — monitoring w produkcji. Nawet najlepsze testy nie gwarantują 100% ochrony przed regresjami. Narzędzia observability (Sentry, Datadog, New Relic) powinny śledzić kluczowe metryki po każdym wdrożeniu: error rate, latency, throughput. Automatyczny wycof (rollback) po przekroczeniu progów — poduszka bezpieczeństwa, jeśli regresja jednak trafi do produkcji.
Czwarty krok — code review z myśleniem regresyjnym. Recenzent powinien zadać pytanie: „Które inne moduły mogą zostać zepsute przez tę zmianę?“. Nie wystarczy sprawdzić, że kod jest poprawny — trzeba sprawdzić, że nie naruszy sąsiedniej funkcjonalności. Lista kontrolna dla code review powinna zawierać punkt „sprawdzenie regresji w sąsiednich modułach“.
Często zadawane pytania
Regresja — to błąd, którego nie było wcześniej. Zwykły błąd mógł istnieć od momentu stworzenia funkcji. Regresja jest zawsze związana z konkretną zmianą — to pozwala użyć git bisect do znalezienia przyczyny.
Użyj git bisect: wskaż commit, gdzie wszystko działało, i commit, gdzie się zepsuło. Git przeprowadzi wyszukiwanie binarne w historii i znajdzie commit, który spowodował regresję. Działa to nawet w dużych projektach z tysiącami commitów.
Nie ma jednoznacznej liczby, ale istnieje empiryczna zasada: pokrycie kluczowych przepływów użytkownika powinno wynosić 100%, pokrycie wszystkich funkcji — co najmniej 70%. Ważniejsza jest jakość niż ilość: test sprawdzający edge case jest cenniejszy niż dziesięć testów na happy path.
Tak, i nazywa się to infrastructure regression. Aktualizacja systemu operacyjnego, wersji bazy danych, certyfikatu SSL lub konfiguracji serwera WWW może zepsuć działający kod. IaC (Infrastructure as Code) i testowanie infrastruktury (Test Kitchen, Terratest) pomagają wykrywać takie regresje.
Zacznij od jednego krytycznego przepływu użytkownika. Napisz automatyczny test dla najważniejszego scenariusza (logowanie, składanie zamówienia). Pokaż na demo, jak test łapie regresję. Gdy zespół zobaczy korzyść — wdrażaj testowanie stopniowo, rozszerzając pokrycie.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również