Testy jednostkowe i integracyjne są fundamentem zaufania do naszego kodu. Problem w tym, że samo napisanie testów tego zaufania jeszcze nie gwarantuje, a nawet doświadczone zespoły .NET regularnie wpadają w pułapki, które sprawiają, że zielony pasek na CI przestaje cokolwiek znaczyć.
Znasz to? Testy przechodzą lokalnie, a na serwerze CI nagle padają - klasyczne "u mnie działa". Albo masz w projekcie testy, które raz są zielone, raz czerwone, bez żadnej zmiany w kodzie (tzw. flaky tests). Bywa też, że pokrywasz tylko szczęśliwą ścieżkę, a produkcja weryfikuje resztę scenariuszy za Ciebie.
W tym artykule przejdziemy przez 5 najczęstszych pułapek przy testowaniu aplikacji .NET i pokażę, jak każdej z nich unikać. Jako lider techniczny możesz na tej podstawie wypracować w zespole lepsze praktyki i realnie wzmocnić zaufanie do wyników testów.
1. Testy zależne od środowiska (efekt "u mnie działa")
Problem: Testy zależne od środowiska to takie, które przechodzą w jednym miejscu, a zawodzą w innym - najczęściej działają na maszynie dewelopera, ale padają na CI/CD. Przyczyn bywa mnóstwo: różnice w konfiguracji (brak zmiennej środowiskowej na CI), inny system lub architektura (Windows lokalnie vs Linux w pipelinie), brak plików czy zasobów dostępnych tylko u Ciebie, inna strefa czasowa albo ustawienia kulturowe (format daty i liczb). Jeśli test opiera się na pliku lub bazie, których na CI po prostu nie ma, jest z góry skazany na porażkę.
Takie testy podkopują zaufanie do całego pakietu, zespół traci czas na zastanawianie się, czy czerwony wynik oznacza błąd w kodzie, czy tylko niespójność środowiska.
Jak unikać: Zasada numer jeden brzmi: odetnij test od specyfiki maszyny, na której się uruchamia.
Nie polegaj na plikach obecnych tylko lokalnie - jeśli test potrzebuje pliku, dołącz go do projektu testowego albo wygeneruj w katalogu tymczasowym w trakcie testu. Konfigurację trzymaj w repozytorium (pliki appsettings, a nie ustawienia zaszyte w lokalnym rejestrze).
Zamiast sięgać po Environment.CurrentDirectory czy zmienne środowiskowe wprost z kodu, wstrzykuj ścieżki i ustawienia przez konstruktor - w testach podstawisz wtedy własne, kontrolowane wartości. Ustawienia wrażliwe na środowisko warto też jawnie "przybić" w teście:
/* Nie pozwól, by test odziedziczył przypadkową kulturę maszyny */
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;Na koniec zadbaj, żeby CI było jak najbliższe środowisku lokalnemu - najprościej przez kontener Dockera z tą samą wersją .NET i systemu. Dzięki temu "działa tylko na mojej maszynie" nigdy nie będzie dotyczyć Waszych testów.
2. Niestabilne testy (flaky tests)
Problem: Niestabilne testy to prawdziwa zmora - potrafią raz przejść, raz się wykrzaczyć, mimo że kod ani drgnął. Dziś zielony, jutro czerwony, za godzinę znów zielony.
Najczęstsze przyczyny to losowość i zależności czasowe: test opierający się na aktualnej dacie, na kolejności wykonywania (działa uruchomiony solo, ale pada w pełnym pakiecie) albo na tym, który wątek zdąży pierwszy (klasyczny race condition). Do tego dochodzą testy integracyjne odwołujące się do zewnętrznych usług - ich wynik zależy od szybkości odpowiedzi serwera czy stanu cudzego API.
To bardzo szkodliwe, bo wprowadza niepewność. Zespół przyzwyczaja się, że "ten test czasem miga na czerwono", zaczyna ignorować czerwone wyniki - i w tym szumie łatwo przeoczyć prawdziwą regresję. Analiza fałszywych alarmów zjada czas i po prostu frustruje.
Jak unikać: Słowo klucz to determinizm - ten sam test przy każdym uruchomieniu ma dawać ten sam wynik.
Unikaj losowości albo ją okiełznaj: jeśli musisz generować losowe dane, ustaw znany seed generatora lub przygotuj stałe próbki. Przy logice zależnej od czasu nie odwołuj się do DateTime.Now wprost - wstrzyknij źródło czasu, które w teście podstawisz na stałą wartość. Od .NET 8 masz do tego wbudowaną abstrakcję TimeProvider (a w testach FakeTimeProvider z pakietu Microsoft.Extensions.TimeProvider.Testing):
var czas = new FakeTimeProvider(
new DateTimeOffset(2025, 1, 1, 12, 0, 0, TimeSpan.Zero));
var token = new TokenService(czas);
czas.Advance(TimeSpan.FromMinutes(30)); /* "przesuwamy zegar" w kontrolowany sposób */
Assert.True(token.CzyWygasl());Przy testach asynchronicznych zawsze await-uj zadania - nie zgaduj czasów przez Thread.Sleep i nie blokuj przez .Wait() czy .Result (to prosta droga do zakleszczeń i niestabilności). Izoluj też testy od siebie: każdy powinien startować z czystym stanem, a wspólne zasoby czy pola statyczne resetuj w metodach inicjalizujących. Dla testów integracyjnych rozważ stuby, mocki lub lokalny serwer-atrapę zamiast realnej sieci.
A jeśli jakiś test dalej bywa niestabilny - nie zamiataj go pod dywan. Możesz go tymczasowo oznaczyć jako [Skip], żeby nie blokował builda, ale potraktuj to jako dług do spłacenia, a nie rozwiązanie docelowe. Cel jest jeden: czerwony = realny błąd, zielony = pewność.
3. Pomijanie nietypowych scenariuszy
Problem: Mamy naturalną skłonność do testowania tylko przewidzianych, typowych ścieżek - happy path. Produkcja szybko ten optymizm weryfikuje, ujawniając błędy tam, gdzie nikt nie zajrzał: przy pustym stringu, null-u, wartościach minimalnych i maksymalnych, duplikatach z bazy, nietypowej odpowiedzi zewnętrznej usługi czy braku uprawnień do pliku.
Efekt jest zdradliwy: pakiet testów daje fałszywe poczucie bezpieczeństwa. Pokrywa dobrze znane przypadki, a aplikacja i tak potyka się na warunkach brzegowych - których jako pierwszy doświadczy użytkownik.
Jak unikać: Zakładaj, że jeśli coś może pójść źle, prędzej czy później pójdzie. Przy projektowaniu każdej funkcji zadawaj pytanie "co jeśli...?". Co jeśli wejście będzie nieprawidłowe? Co jeśli warstwa niżej rzuci wyjątkiem? Co jeśli lista jest pusta, ma jeden element, a co jeśli ma sto tysięcy?
W praktyce oznacza to dopisywanie testów na przypadki skrajne i błędne. Sprawdzaj, czy metoda rzuca właściwym wyjątkiem przy złych parametrach:
Assert.Throws<ArgumentException>(() => kalkulator.Podziel(10, 0));Świetnym narzędziem są tu testy parametryzowane - w xUnit [Theory] z [InlineData] pozwala pokryć wiele wariantów bez kopiowania kodu:
[Theory]
[InlineData("")]
[InlineData(null)]
[InlineData(" ")]
public void ZwracaBlad_DlaPustegoLoginu(string login) =>
Assert.False(walidator.CzyPoprawny(login));Nie zapominaj też o testach negatywnych, które potwierdzają, że system poprawnie odrzuca złe dane. Uwzględniając nietypowe scenariusze, robisz z pakietu testów coś kompletnego i odpornego na niespodzianki.
4. Kruche testy zależne od implementacji
Problem: Kolejna pułapka to testy, które zbyt mocno zaglądają w bebechy testowanej klasy. Nazywamy je kruchymi, bo psuje je nawet drobna zmiana wewnętrzna - mimo że z perspektywy użytkownika nic się nie zmieniło. Test weryfikujący kolejność wywołań prywatnych metod czy wartości prywatnych pól zacznie krzyczeć po każdej refaktoryzacji, choć kontrakt zewnętrzny pozostał ten sam.
Efekt? Programiści dochodzą do wniosku, że "testy przeszkadzają w zmianach", i zaczynają je usuwać albo wyłączać. Nadwrażliwe testy podważają sens całego testowania - zamiast chronić przed regresją, generują fałszywe alarmy i spowalniają rozwój.
Jak unikać: Testuj zachowanie, nie implementację. Traktuj testowaną klasę jak czarną skrzynkę - interesuje Cię, co zwraca i jak zmienia widoczny na zewnątrz stan, a nie jak to robi w środku. Unikaj asercji na wewnętrznych polach i nie nadużywaj mocków: sprawdzaj interakcje na poziomie kontraktu, a nie konkretnej, nieistotnej biznesowo kolejności wywołań.
Dobry test jednostkowy przechodzi po bezpiecznej refaktoryzacji, o ile wymagania i wynik się nie zmieniły. Jeśli metoda sortująca zacznie używać innego algorytmu, test sprawdzający poprawność posortowania dalej działa - ale test liczący iteracje pętli już nie. Pisz testy skupione na rezultatach, regułach biznesowych i efektach dla użytkownika, a pakiet stanie się stabilnym zabezpieczeniem, nie kajdanami.
5. Ignorowanie niezdanych testów
Problem: Ostatnia pułapka to godzenie się z testami, o których wiemy, że są wadliwe - i nierobienie z tym niczego. W pakiecie tkwi kilka chronicznie czerwonych testów albo takie, które ktoś kiedyś oznaczył jako [Skip], "bo od dawna się psuły". Brzmi niewinnie ("to tylko testy, kod działa"), a jest naprawdę groźne.
Po pierwsze, gdy przyzwyczaimy się, że część testów zawsze świeci na czerwono, prawdziwa regresja ginie w szumie znanych awarii. Po drugie, wyłączanie zamiast naprawiania to powolna erozja pokrycia - dziś pominiemy jeden flaky test integracyjny, jutro dwa jednostkowe po refaktoryzacji, i nagle spora część logiki nie jest już chroniona. Tolerowanie zepsutych testów to prosty przepis na utrwalanie błędów i utratę zaufania do wyników.
Jak unikać: Reaguj na czerwień od razu. Traktuj trwale niezdany test tak samo poważnie jak buga na produkcji - bo często jedno wynika z drugiego. Jeśli test ujawnia realny błąd, zaplanuj naprawę jak najszybciej. Jeśli to sam test jest wadliwy, też go napraw - a gdy musisz go tymczasowo wyłączyć, rób to z jasnym terminem powrotu, a nie "na zawsze".
Ustalcie w zespole prostą zasadę: build na CI nie może mieć czerwonych testów, a dnia nie kończymy z czerwonym paskiem. Jako lider techniczny bądź przykładem - regularnie przeglądaj raporty, usuwaj testy, które straciły sens, i przypominaj o wartości zielonego pakietu. Dzięki temu zespół nie popadnie w znieczulenie na czerwień, a testy pozostaną w pełni użyteczne.
Podsumowanie
Testy automatyczne mają dawać pewność, że kod działa i że zmiany nie psują istniejących funkcji. Ale ta pewność istnieje tylko wtedy, gdy możemy zaufać samym testom. Unikając pięciu opisanych pułapek - testów zależnych od środowiska, niestabilnych wyników, luk w scenariuszach brzegowych, kruchych testów zależnych od implementacji i ignorowania czerwonych wyników - realnie podnosisz wiarygodność i wartość całego pakietu.
Jako doświadczony programista czy lider techniczny warto dzielić się tymi praktykami z zespołem i pielęgnować kulturę "zielonego builda".
Solidne testowanie to umiejętność, którą buduje się stopniowo - jeden dobry nawyk po drugim. Jeśli chcesz regularnie dostawać takie praktyczne wskazówki o testowaniu i pisaniu lepszego kodu w .NET prosto na swoją skrzynkę, dołącz do mojej listy VIP: modestprogrammer.pl/vip.
To miejsce, w którym dzielę się rzeczami, których nie znajdziesz na blogu - kulisami rozwiązywania realnych problemów, konkretnymi przykładami z kodu i materiałami, które pomogą Tobie i Twojemu zespołowi testować z większą pewnością. Zero spamu, wyłącznie wiedza, którą wykorzystasz w pracy od razu następnego dnia.
Powodzenia w testowaniu.