Czy ręczne pisanie testów jednostkowych jest dowodem na przykład?

9

Wiemy, że pisanie testów JUnit pokazuje jedną konkretną ścieżkę przez twój kod.

Jeden z moich współpracowników skomentował:

Ręczne pisanie testów jednostkowych jest dowodem na przykład .

Pochodził z Haskell, który ma narzędzia takie jak Quickcheck i umiejętność rozumowania zachowania programu za pomocą typów .

Jego implikacją było to, że istnieje wiele innych kombinacji danych wejściowych, które nie są sprawdzane przez tę metodę, dla których twój kod nie jest testowany.

Moje pytanie brzmi: czy ręczne pisanie testów jednostkowych jest dowodem na przykład?

Sokole Oko
źródło
3
Nie, nie pisz / nie używaj testów. Twierdzenie, że twoje testy jednostkowe są dowodem na to, że nie ma nic złego w programie, to Dowód na przykład (niewłaściwe uogólnienie). Testy nie polegają na matematycznym sprawdzaniu poprawności kodu - testy są z natury kontrolnymi eksperymentami. Jest to sieć bezpieczeństwa, która pomaga budować zaufanie, mówiąc coś o kodzie. Ale to ty musisz wybrać dobrą strategię sondowania kodu i to ty musisz interpretować znaczenie tych danych.
Filip Milovanović,

Odpowiedzi:

10

Jeśli losowo wybierasz dane wejściowe do testowania, to przypuszczam, że możliwe jest, że używasz logicznego błędnego dowodu na podstawie przykładu.

Ale dobre testy jednostkowe nigdy tego nie robią. Zamiast tego zajmują się zakresami i przypadkami krawędzi.

Na przykład, jeśli napiszesz testy jednostkowe dla funkcji wartości bezwzględnej, która przyjmuje liczbę całkowitą jako dane wejściowe, nie musisz testować każdej możliwej wartości danych wejściowych, aby udowodnić, że kod działa. Aby uzyskać kompleksowy test, potrzebujesz tylko pięciu wartości: -1, 0, 1 oraz wartości maksymalnej i minimalnej dla wejściowej liczby całkowitej.

Te pięć wartości testuje każdy możliwy zakres i przypadek krawędzi funkcji. Nie trzeba testować każdej innej możliwej wartości wejściowej (tj. Każdej liczby, którą może reprezentować typ całkowity), aby uzyskać wysoki poziom pewności, że funkcja działa dla wszystkich wartości wejściowych.

Robert Harvey
źródło
11
Tester kodów wchodzi do baru i zamawia piwo. 5 piw. -1 piwa, piwa MAX_VALUE, kurczak. zero.
Neil,
2
„5 wartości” to czysty nonsens. Rozważmy taką trywialną funkcję jak int foo(int x) { return 1234/(x - 100); }. Zauważ również, że (w zależności od tego, co testujesz) może być konieczne upewnienie się, że nieprawidłowe („poza zakresem”) dane wejściowe zwracają prawidłowe wyniki (np. Że `` find_thing (rzecz) `poprawnie zwraca pewien rodzaj statusu„ nie znaleziono ” jeśli czegoś nie znaleziono).
Brendan
3
@Brendan: Nie ma nic znaczącego w tym, że jest to pięć wartości; w moim przykładzie jest to po prostu pięć wartości. Twój przykład ma inną liczbę testów, ponieważ testujesz inną funkcję. Nie twierdzę, że każda funkcja wymaga dokładnie pięciu testów; wywnioskowałeś to po przeczytaniu mojej odpowiedzi.
Robert Harvey
1
Biblioteki testów generatywnych są zwykle lepsze w testowaniu przypadków skrajnych niż Ty. Jeśli, na przykład, uzywasz pływaków zamiast liczb całkowitych, biblioteka będzie również sprawdzić -Inf, Inf, NaN, 1e-100, -1e-100, -0, 2e200... Wolałabym nie trzeba robić tych wszystkich ręcznie.
Hovercouch,
@Hovercouch: Jeśli znasz dobry, chciałbym o nim usłyszeć. Najlepszy, jaki widziałem, to Pex; była jednak niesamowicie niestabilna. Pamiętaj, że mówimy tutaj o stosunkowo prostych funkcjach. Sprawy stają się trudniejsze, gdy mamy do czynienia z takimi rzeczami, jak logika biznesowa.
Robert Harvey
8

Każde testowanie oprogramowania przypomina „Dowód przez przykład”, a nie tylko testowanie jednostkowe przy użyciu narzędzia takiego jak JUnit. I to nie jest nowa mądrość, jest cytat z Dijkstry z 1960 roku, który mówi w zasadzie to samo:

„Testowanie pokazuje obecność, a nie brak błędów”

(wystarczy zamienić słowa „pokazuje” na „dowody”). Dotyczy to jednak również narzędzi generujących losowe dane testowe. Liczba możliwych danych wejściowych dla funkcji w świecie rzeczywistym jest zwykle większa o rząd wielkości niż liczba przypadków testowych, które można wytworzyć i zweryfikować w oparciu o oczekiwany wynik w epoce wszechświata, niezależnie od metody generowania tych przypadków, więc nawet jeśli do generowania dużej ilości danych testowych używa się narzędzia generatora, nie ma gwarancji, że nie pominie się jednego przypadku testowego, który mógł wykryć określony błąd.

Losowe testy mogą czasami ujawnić błąd, który został przeoczony przez ręcznie utworzone przypadki testowe. Ale ogólnie rzecz biorąc, bardziej efektywne jest staranne wykonanie testów do testowanej funkcji i upewnienie się, że uzyskano pełny kod i pokrycie gałęzi przy jak najmniejszej liczbie przypadków testowych. Czasami jest wykonalną strategią łączenie testów generowanych ręcznie i losowo. Ponadto przy stosowaniu testów losowych należy zadbać o to, aby wyniki były odtwarzalne.

Dlatego ręcznie tworzone testy nie są gorsze od losowo generowanych testów, często wręcz przeciwnie.

Doktor Brown
źródło
1
Każdy praktyczny zestaw testów wykorzystujący losowe sprawdzanie będzie również zawierał testy jednostkowe. (Technicznie testy jednostkowe są tylko zdegenerowanym przypadkiem testowania losowego.) Twoje sformułowanie sugeruje, że testy randomizowane są trudne do osiągnięcia lub że połączenie testów losowych z testami jednostkowymi jest trudne. Zazwyczaj tak nie jest. Moim zdaniem jedną z największych zalet losowego testowania jest to, że zdecydowanie zachęca do pisania testów jako właściwości dotyczących kodu, które mają być zawsze przechowywane. Wolałbym, aby te właściwości zostały wyraźnie określone (i sprawdzone!), Niż wnioskować o nich w punktowych testach.
Derek Elkins opuścił SE
@DerekElkins: „trudny” to zły pomysł IMHO. Losowe testy wymagają sporo wysiłku, a to jest wysiłek, który skraca dostępny czas na testy rzemieślnicze (a jeśli masz ludzi, którzy tylko przestrzegają haseł takich jak wspomniane w pytaniu, prawdopodobnie nie wykonają żadnego rękodzieła). Samo wrzucenie wielu losowych danych testowych na fragment kodu to tylko połowa pracy, należy również uzyskać oczekiwane wyniki dla każdego z tych danych wejściowych testu. W przypadku niektórych scenariuszy można to zrobić automatycznie. Dla innych nie.
Doc Brown
Chociaż zdecydowanie są chwile, kiedy trzeba przemyśleć, aby wybrać dobrą dystrybucję, zwykle nie jest to poważny problem. Twój komentarz sugeruje, że myślisz o tym w niewłaściwy sposób. Właściwości, które piszesz do losowego sprawdzania, są tymi samymi właściwościami, które piszesz do sprawdzania modelu lub do formalnych dowodów. Rzeczywiście, mogą być i były używane do wszystkich tych rzeczy jednocześnie. Nie ma też „oczekiwanych rezultatów”, które należy uzyskać. Zamiast tego podajesz po prostu właściwość, która zawsze powinna się trzymać. Kilka przykładów: 1) wepchnięcie czegoś na stos i ...
Derek Elkins opuścił SE
... wtedy strzelanie powinno być tym samym, co nic nie robienie; 2) każdy klient z saldem większym niż 10 000 USD powinien uzyskać oprocentowanie o wysokim saldzie i tylko wtedy; 3) pozycja duszka zawsze znajduje się w obwiedni ekranu. Niektóre właściwości mogą równie dobrze odpowiadać testom punktowym, np. „Gdy saldo wynosi 0 USD, ostrzeżenie o zerowym saldzie”. Właściwości są częściowymi specyfikacjami, a ideał uzyskania pełnej specyfikacji. Trudność z wymyśleniem tych właściwości oznacza, że ​​nie jesteś pewien, co to jest specyfikacja, a często oznacza, że ​​będziesz miał trudności z wymyśleniem dobrych testów jednostkowych.
Derek Elkins opuścił SE
0

Ręczne pisanie testów to „dowód na przykład”. Ale to samo dotyczy QuickCheck i w ograniczonym zakresie typów systemów. Wszystko, co nie jest prostą formalną weryfikacją, będzie ograniczone w tym, co mówi ci o twoim kodzie. Zamiast tego musisz myśleć w kategoriach względnej wartości podejść.

Testy generatywne, takie jak QuickCheck, są naprawdę dobre do zamiatania szerokiej gamy danych wejściowych. Jest również o wiele lepszy w rozwiązywaniu przypadkowych przypadków niż w testach ręcznych: biblioteki testów generatywnych będą z tym bardziej doświadczone niż ty. Z drugiej strony mówią tylko o niezmiennikach, a nie o konkretnych wynikach. Tak więc, aby sprawdzić, czy Twój program uzyskuje prawidłowe wyniki, nadal potrzebujesz testów ręcznych, aby to sprawdzić foo(bar) = baz.

Hovercouch
źródło