Poza procentem objęcia: testowanie błędów, z którymi faktycznie spotykają się użytkownicy
Dlaczego wysoki wskaźnik pokrycia kodu może ukrywać nieprzetestowane przypadki awarii, trzy rodzaje prób pozorowych, do których skłania, oraz jak pisać testy chroniące rzeczywiste zachowanie.
Zapytanie typu pull request z wszystkimi testami jako „zielonymi” oraz raportem pokrycia powyżej 98% sprawia wrażenie bezpieczne. Jednak gdy API zwraca null tam, gdzie interfejs oczekiwał obiektu, żądanie jest realizowane w niewłaściwej kolejności przy wolnym połączeniu mobilnym, lub ktoś dwukrotnie kliknie przycisk wysłania, interfejs użytkownika zamienia się w pustą ekran. Pytanie po analizie awarii jest zawsze takie samo: jak to się mogło zepsuć, skoro plik był w pełni przetestowany? Ten artykuł wyjaśnia, co tak naprawdę mierzy pokrycie, wzorce testowe, które je zawyżają bez zapewniania ochrony, oraz jak dostosować zestaw testów tak, by skupiał się na tych awariach, które są istotne.
Co mierzy pokrycie, a czego nie
Narzędzie do pomiaru zakresu rejestruje, które linie, gałęzie i funkcje zostały wykonyane podczas działania testów. To wszystko. Nie wie ono, czy jakiekolwiek założenia sprawdziły wynik, czy dane wejściowe przypominały rzeczywisty ruch, ani też czy kod zachowuje się prawidłowo, gdy jakaś zależność działa niewłaściwie. Wykonanie i weryfikacja to dwie różne cechy, a narzędzie do pomiaru zakresu podaje informacje tylko o pierwszej.
Potrzebną analogią jest inspekcja budynku, podczas której ktoś przechodzi przez każde pomieszczenie z latarką. Każde pomieszczenie zostało sprawdzone, ale nikt nie sprawdził dachu podczas burzy. Wysoki zakres oznacza jedynie, że testy dotarły do kodu, a nie to, że rzuciły mu wyzwanie.
Trzy wzorce, które zwiększają zakres bez dodawania bezpieczeństwa
Gdy celem staje się procent, ludzie optymalizują działania pod kątem tego procentu. W rezultacie powstają testy, które z minimalnym wysiłkiem zadowalają narzędzie. Ponawiają się trzy określone wzorce.
Miraż ścieżki sukcesu
Załóżmy narzędzie, które analizuje dane wprowadzone przez użytkownika i aktualizuje stan. Jego test przepuszcza poprawny, dobrze sformatowany ciąg znaków, sprawdza oczekiwany wynik i osiąga pełne pokrycie gałęzi. Nigdy jednak nie testuje pustego ciągu znaków, nietypowych liter, argumentu undefined, wolnej odpowiedzi czy niepoprawnego JSON. Każda linia kodu została wykonywana, ale nie sprawdzono żadnych danych wejściowych, które mogą powodować problemy w produkcji.
Komponent nadmiernie mockowany
Tutaj każda wywołanie API, dostawca kontekstu oraz zagnieżdżone elementy są zastępowane przez sztuczne odpowiedniki. Testy kończą się w ciągu kilku milisekund i obejmują wszystkie możliwe ścieżki renderowania. W warunkach produkcyjnych jednak prawdziwy punkt końcowy zwraca nieco inny format danych niż zakładano w symulacji, albo biblioteka zmienia sposób emitowania zdarzeń po aktualizacji. Testy nadal przepuszczają testy, ponieważ odnoszą się wyłącznie do własnych założeń, a pierwsza rzeczywista integracja następuje w przeglądarce użytkownika.
Testy bez istotnych stwierdzeń
Najslabsza forma ta występuje przy ścisłych wymaganiach, takich jak konieczny próg 90% w całym zespole. Testy wywołują funkcje wyłącznie po to, by odnotować wykonanie linii kodu, i stwierdzają niewiele lub w ogóle nic. Raport zmienia kolor na zielony, mimo że nie ma żadnej bariery chroniącej kod przed przyszłymi zmianami logiki.
Jak testować zachowanie zamiast liczby linii kodu
Testy, które wykrywają błędy przed użytkownikami, koncentrują się na tym, co robi system, a nie na tych liniach kodu, które są przez niego dotykane.
- Testuje się stany i przejścia, a nie pojedyncze funkcje. Użytkownikom nie zależy na tym, czy uruchomił się jakiś pomocnik. Zależy im na tym, co się dzieje, gdy połączenie internetowe zostanie przerwane w trakcie wysyłania formularza lub gdy próbują opuścić stronę, podczas gdy przesyłanie pliku wciąż trwa. Należy sprawdzić wskaźniki ładowania, stany błędów, możliwości ponownej próby oraz mechanizmy radzenia sobie z błędami.
- Należy preferować integrację tam, gdzie jest to praktyczne. Symulowanie prawdziwych zewnętrznych elementów, takich jak serwer płatności third-party, jest rozsądne. Symulowanie własnych wewnętrznych narzędzi lub warstwy danych najczęściej ukrywa błędy występujące pomiędzy nimi. Pozwól komponentom i modułom działać razem w takim stopniu, na jaki pozwala koszt.
Związaną i dobrze udokumentowaną techniką jest testowanie mutacji, polegające na celowym modyfikowaniu kodu (zmianie warunku, usunięciu wiersza) i sprawdzaniu, czy jakikolwiek test zawiedzie. Mutacje, które przetrwały, wskazują bezpośrednio na kod, który jest wykonywany, ale nie sprawdzany – to właśnie tego pokrycie nie jest w stanie wykazać. Aby dowiedzieć się więcej na temat szczegółów na poziomie komponentów, zapoznaj się z tymi antypatronami testowania w React, które tworzą fałszywe poczucie pewności.
Wykorzystywanie pokrycia jako sygnału, a nie celu
Wskaźnik pokrycia nie jest bezużyteczny. Niska wartość w kluczowym module stanowi prawdziwe ostrzeżenie, a raporty mogą ujawnić ścieżki kodu, których w ogóle nikt nie testuje. Problem pojawia się, gdy celem staje się sam procent, ponieważ wtedy nagradza się ilość zamiast rygoru.
Optymalny zestaw testów o wskaźniku pokrycia około 65%, skupiony na ryzykownych procesach, złożonych regułach biznesowych oraz mechanizmach przywracania po awarii, zapobiegnie znacznie większej liczbie incydentów niż nieodporny zestaw o wskaźniku 95%, składający się z testów na bezproblemowe ścieżki działania i symulacji. Zanim dodamy nowy test, lepszym pytaniem niż „które wiersze nie zostały objęte testem?” jest „jaką rzeczywistą awarię ten test wykryje?”
Główne wnioski
- Wskaźnik pokrycia pokazuje, który kod został uruchomiony, a nie to, czy jego zachowanie zostało sprawdzone.
- Dane dla bezproblemowych ścieżek działania, intensywne symulacje wewnętrzne oraz testy bez twierdzeń zwiększają wartość wskaźnika, nie zmniejszając przy tym ryzyka.
Literatura pokrewna
- Poza rozmiarem pakietu: Odkrywanie tego, co naprawdę spowalnia twoją aplikację webową — Dlaczego redukcja liczby kilobajtów rzadko naprawia wolną aplikację oraz jak śledzić rzeczywisty czas oczekiwania na serwerach, w procesach typu waterfall, przy hydratacji stron, skryptach i obrazach od dostawców zewnętrznych.
- Poza P95: Mierzenie opóźnienia, którego faktycznie doświadczają twoi użytkownicy — Dlaczego zdrowe wartości P95 mogą współistnieć z powolnym produktem, jak czas oczekiwania w kolejce i rozprzestrzenianie zapytań ukrywają się przed panelami kontrolnymi, oraz jak mierzenie czasu na poszczególne kroki kończy gry w przypisywanie winy za opóźnienia.