Strona główna / Artykuły / Poza procentem objęcia: testowanie błędów, z którymi faktycznie spotykają się użytkownicy

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.

920 słów

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.
  • Pozwól błędom produkcyjnym rozszerzać zestaw testów. Gdy pojawi się defekt, napisz test, który go odtworzy i zawiedzie, zanim zaczniesz pracować nad jego naprawą, a następnie modyfikuj kod, aż test przejdzie. Z czasem zestaw testów gromadzi przypadki krawędziowe, z którymi faktycznie boryka się system, a nie te, które ktoś sobie wyobraził.
  • 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.
  • Przejścia między stanami docelowymi, obsługa błędów oraz integracja pomiędzy własnymi modułami.
  • Najpierw zamień każdy wykryty błąd w nieudany test, a dopiero potem go napraw.
  • Śledź tryby awarii, przed którymi chroni twój zestaw testów, i traktuj wskaźnik pokrycia jako pomocowe wskazówki.
  • Literatura pokrewna