Jeśli testowana jest każda ścieżka przez program, czy gwarantuje to znalezienie wszystkich błędów?
Jeśli nie, dlaczego nie? Jak można przejść przez każdą możliwą kombinację przebiegu programu i nie znaleźć problemu, jeśli taki istnieje?
Waham się z sugestią, że można znaleźć „wszystkie błędy”, ale może dlatego, że pokrycie ścieżki nie jest praktyczne (ponieważ jest kombinatoryczne), więc nigdy nie jest doświadczane?
Uwaga: ten artykuł zawiera krótkie podsumowanie rodzajów pokrycia, gdy o nich myślę.
Odpowiedzi:
Nie
Ponieważ nawet jeśli przetestujesz wszystkie możliwe ścieżki , nadal nie przetestowałeś ich ze wszystkimi możliwymi wartościami lub wszystkimi możliwymi kombinacjami wartości . Na przykład (pseudokod):
źródło
Oprócz odpowiedzi Masona istnieje jeszcze jeden problem: zasięg nie mówi, jaki kod został przetestowany, ale mówi, który kod został wykonany .
Wyobraź sobie, że masz testsuite ze 100% pokryciem trasy. Teraz usuń wszystkie asercje i ponownie uruchom testsuite. Voilà, testsuite wciąż ma 100% pokrycia trasy, ale absolutnie nic nie testuje.
źródło
ON ERROR GOTO
to również droga, jak jest na Cif(errno)
.Oto prostszy przykład na zakończenie. Rozważ następujący algorytm sortowania (w Javie):
Teraz przetestujmy:
Teraz rozważmy, że (A) to konkretne wywołanie
sort
zwraca poprawny wynik, (B) wszystkie ścieżki kodu zostały objęte tym testem.Ale oczywiście program nie sortuje.
Wynika z tego, że pokrycie wszystkich ścieżek kodu nie jest wystarczające, aby zagwarantować, że program nie zawiera błędów.
źródło
Rozważ
abs
funkcję, która zwraca wartość bezwzględną liczby. Oto test (Python, wyobraź sobie jakiś szkielet testowy):Ta implementacja jest poprawna, ale zapewnia jedynie 60% pokrycia kodu:
Ta implementacja jest niepoprawna, ale zapewnia 100% pokrycia kodu:
źródło
def abs(x): if x == -3: return 3 else: return 0
Możliwe, że pominiesz tęelse: return 0
część i uzyskasz 100% pokrycia, ale funkcja byłaby zasadniczo bezużyteczna, nawet jeśli przejdzie test jednostkowy.Kolejny dodatek do odpowiedzi Masona , zachowanie programu może zależeć od środowiska wykonawczego.
Poniższy kod zawiera opcję Use-After-Free:
Ten kod jest niezdefiniowanym zachowaniem, w zależności od konfiguracji (wydania | debugowania), systemu operacyjnego i kompilatora, przyniesie różne zachowania. Zasięg ścieżki nie tylko nie gwarantuje, że znajdziesz UAF, ale Twój zestaw testów zazwyczaj nie obejmuje różnych możliwych zachowań UAF, które zależą od konfiguracji.
Z drugiej strony, nawet jeśli pokrycie ścieżki gwarantowałoby znalezienie wszystkich błędów, jest mało prawdopodobne, że można to osiągnąć w praktyce w dowolnym programie. Rozważ następujące:
Jeśli Twój pakiet testowy może wygenerować wszystkie ścieżki do tego, gratulacje, jesteś kryptografem.
źródło
cryptohash
, trudno jest powiedzieć, czym jest „wystarczająco mały”. Może ukończenie superkalkulatora zajmuje dwa dni. Ale tak,int
może okazać się trochęshort
.Z pozostałych odpowiedzi jasno wynika, że 100% pokrycia kodu w testach nie oznacza 100% poprawności kodu, ani nawet że wszystkie błędy, które można znaleźć podczas testowania, zostaną znalezione (nie wspominając o błędach, których żaden test nie może wyłapać).
Innym sposobem odpowiedzi na to pytanie jest praktyka:
W prawdziwym świecie, a nawet na własnym komputerze, jest wiele programów, które są opracowywane przy użyciu zestawu testów, które zapewniają 100% pokrycia, a które nadal zawierają błędy, w tym błędy, które można by lepiej zidentyfikować.
W związku z tym powstaje pytanie:
Narzędzia pokrycia kodu pomagają zidentyfikować obszary, których testowanie zostało zaniedbane. To może być w porządku (kod jest wyraźnie poprawny nawet bez testowania), może być niemożliwe do rozwiązania (z jakiegoś powodu nie można trafić na ścieżkę), lub może być lokalizacją wielkiego śmierdzącego błędu albo teraz, albo po przyszłych modyfikacjach.
Pod pewnymi względami sprawdzanie pisowni jest porównywalne: Coś może „przejść” sprawdzanie pisowni i zostać błędnie napisane w taki sposób, aby pasowało do słowa w słowniku. Lub może „zawieść”, ponieważ poprawnych słów nie ma w słowniku. Lub może przejść i być kompletnym nonsensem. Sprawdzanie pisowni to narzędzie, które pomaga zidentyfikować miejsca, które mogły zostać pominięte podczas korekty, ale podobnie jak nie może zagwarantować pełnego i prawidłowego odczytu, tak więc pokrycie kodu nie może zagwarantować pełnego i poprawnego testowania.
I oczywiście niewłaściwy sposób użycia sprawdzania pisowni jest znany z każdej sugestii, jaką sugeruje owca morska, więc sytuacja kaczki staje się gorsza niż wtedy, gdy zostawiamy pożyczkę.
Z pokryciem kodu może być kuszące, zwłaszcza jeśli masz prawie idealne 98%, wypełnianie skrzynek, tak aby pozostałe ścieżki zostały trafione.
Jest to odpowiednik wyrównywania za pomocą sprawdzania pisowni, że wszystkie słowa to pogoda lub węzeł to wszystkie odpowiednie słowa. Rezultatem jest bałaganiarski bałagan.
Jeśli jednak zastanowisz się, jakie testy naprawdę nieobjęte ścieżki naprawdę potrzebują, narzędzie do pokrywania kodu wykona swoje zadanie; nie obiecując ci poprawności, ale wskazując niektóre prace, które należało wykonać.
źródło
Zasięg ścieżki nie pozwala stwierdzić, czy wszystkie wymagane funkcje zostały zaimplementowane. Pozostawienie funkcji jest błędem, ale pokrycie ścieżki jej nie wykryje.
źródło
Częścią problemu jest to, że 100% pokrycia gwarantuje tylko, że kod będzie działał poprawnie po jednym wykonaniu . Niektóre błędy, takie jak wycieki pamięci, mogą nie być widoczne lub powodować problemy po jednym wykonaniu, ale z czasem będą powodować problemy dla aplikacji.
Załóżmy na przykład, że masz aplikację, która łączy się z bazą danych. Być może w jednej z metod programista zapomina o zamknięciu połączenia z bazą danych po zakończeniu zapytania. Można uruchomić kilka testów tej metody i nie wykryć żadnych błędów w jej działaniu, ale serwer bazy danych może napotkać scenariusz, w którym nie ma dostępnych połączeń, ponieważ ta konkretna metoda nie zamknęła połączenia, gdy zostało wykonane, a otwarte połączenia muszą teraz limit czasu.
źródło
times_two(x) = x + 2
, zostanie to w pełni objęte pakietem testowymassert(times_two(2) == 4)
, ale nadal jest to oczywiście błędny kod! Nie ma potrzeby wycieków pamięci :)Jak już powiedziano, odpowiedź brzmi NIE.
Poza tym, co się mówi, na różnych poziomach pojawiają się błędy, których nie można przetestować za pomocą testów jednostkowych. Wystarczy wspomnieć o kilku:
źródło
Co to znaczy dla każdej ścieżki do przetestowania?
Inne odpowiedzi są świetne, ale chcę tylko dodać, że sam warunek „każda ścieżka przez program jest testowany” jest niejasny.
Rozważ tę metodę:
Jeśli napiszesz test, który potwierdza
add(1, 2) == 3
, narzędzie do pokrycia kodu powie ci, że każda linia jest wykonywana. Ale tak naprawdę nie twierdziłeś nic o globalnym skutku ubocznym lub bezużytecznym zadaniu. Te wiersze zostały wykonane, ale tak naprawdę nie zostały przetestowane.Testowanie mutacji pomogłoby znaleźć takie problemy. Narzędzie do testowania mutacji ma listę z góry określonych sposobów „mutowania” kodu i sprawdzania, czy testy nadal przebiegają pomyślnie. Na przykład:
+=
się-=
. Ta mutacja nie spowodowałaby niepowodzenia testu, więc udowodniłaby, że test nie potwierdza niczego znaczącego na temat globalnego efektu ubocznego.W skrócie, testy mutacji są sposobem na przetestowanie testów . Ale tak jak nigdy nie przetestujesz rzeczywistej funkcji przy każdym możliwym zestawie danych wejściowych, nigdy nie uruchomisz każdej możliwej mutacji, więc znowu jest to ograniczone.
Każdy test, który możemy wykonać, jest heurystyczny w kierunku programów wolnych od błędów. Nic nie jest doskonałe.
źródło
Cóż ... tak, właściwie, jeśli każda ścieżka „przez” program jest testowana. Ale to oznacza, że każda możliwa ścieżka przez całą przestrzeń wszystkich możliwych stanów, jakie może mieć program, łącznie ze wszystkimi zmiennymi. Nawet w przypadku bardzo prostego statycznie skompilowanego programu - powiedzmy, starego crunchera liczb Fortran - nie jest to wykonalne, chociaż może to być przynajmniej możliwe: jeśli masz tylko dwie zmienne całkowite, masz do czynienia ze wszystkimi możliwymi sposobami łączenia punktów na dwuwymiarowa siatka; w rzeczywistości przypomina bardzo Traveling Salesman. Dla n takich zmiennych mamy do czynienia z przestrzenią n- wymiarową, więc dla każdego prawdziwego programu zadanie jest całkowicie niewykonalne.
Gorzej: poważnie mówiąc, masz nie tylko stałą liczbę prymitywnych zmiennych, ale tworzysz zmienne w locie w wywołaniach funkcji lub masz zmienne o zmiennej wielkości ... lub cokolwiek podobnego, jak to możliwe, w języku kompletnym Turinga. To sprawia, że przestrzeń stanu jest nieskończenie wymiarowa, niszcząc wszelkie nadzieje na pełne pokrycie, nawet biorąc pod uwagę absurdalnie potężny sprzęt testowy.
To powiedziawszy ... w rzeczywistości rzeczy nie są tak ponure. To jest możliwe, aby udowodnić całych programów za prawidłowe, ale będziesz musiał zrezygnować z kilku pomysłów.
Po pierwsze: wysoce wskazane jest przejście na deklaratywny język. Języki imperatywne, z jakiegoś powodu, zawsze były zdecydowanie najbardziej popularne, ale sposób ich mieszać ze sobą algorytmów z interakcji w świecie rzeczywistym sprawia, że niezwykle trudno nawet powiedzieć, co masz na myśli przez „poprawne”.
Znacznie łatwiej w czysto funkcjonalnych językach programowania: mają one wyraźne rozróżnienie między naprawdę interesującymi właściwościami funkcji matematycznych a rozmytymi interakcjami w świecie rzeczywistym, o których tak naprawdę nie można nic powiedzieć. W przypadku funkcji bardzo łatwo jest określić „prawidłowe zachowanie”: jeśli dla wszystkich możliwych danych wejściowych (z typów argumentów) pojawi się odpowiedni pożądany wynik, wówczas funkcja zachowuje się poprawnie.
Mówicie, że wciąż jest to trudne ... w końcu przestrzeń wszystkich możliwych argumentów jest na ogół również nieskończona. To prawda - choć w przypadku jednej funkcji nawet naiwne testowanie zasięgu prowadzi o wiele dalej, niż można się spodziewać w programie imperatywnym! Istnieje jednak niesamowicie potężne narzędzie, które zmienia grę: uniwersalna kwantyfikacja / polimorfizm parametryczny . Zasadniczo pozwala to na pisanie funkcji na bardzo ogólnych rodzajach danych, z gwarancją, że jeśli działa na prostym przykładzie danych, będzie działał dla każdego możliwego wejścia.
Przynajmniej teoretycznie. Nie jest łatwo znaleźć odpowiednie typy, które są tak ogólne, że możesz to całkowicie udowodnić - zwykle potrzebujesz języka o typie zależnym , a te są raczej trudne w użyciu. Ale pisanie w funkcjonalnym stylu z samym polimorfizmem parametrycznym już wspaniale podnosi „poziom bezpieczeństwa” - niekoniecznie znajdziesz wszystkie błędy, ale będziesz musiał je całkiem dobrze ukryć, aby kompilator ich nie wykrył!
źródło