Jak piszesz testy jednostkowe kodu z trudnymi do przewidzenia wynikami?

124

Często pracuję z programami numerycznymi / matematycznymi, w których dokładny wynik funkcji jest trudny do przewidzenia z góry.

Próbując zastosować TDD z tego rodzaju kodem, często uważam, że pisanie testowanego kodu jest znacznie łatwiejsze niż pisanie testów jednostkowych dla tego kodu, ponieważ jedynym sposobem na znalezienie oczekiwanego wyniku jest zastosowanie samego algorytmu (czy w moim głowa, na papierze lub przy komputerze). To wydaje się błędne, ponieważ skutecznie używam testowanego kodu do weryfikacji moich testów jednostkowych, zamiast na odwrót.

Czy znane są techniki pisania testów jednostkowych i stosowania TDD, gdy trudno jest przewidzieć wynik testowanego kodu?

(Prawdziwy) przykład kodu z trudnymi do przewidzenia wynikami:

Funkcja, weightedTasksOnTimektóra biorąc pod uwagę ilość pracy wykonanej dziennie workPerDayw zakresie (0, 24], aktualny czas initialTime> 0 i listę zadań taskArray; każde z czasem do ukończenia właściwości time> 0, terminem duei wartością ważności importance; zwraca znormalizowana wartość z zakresu [0, 1] reprezentująca znaczenie zadań, które można wykonać przed ich duedatą, jeżeli każde zadanie zostanie wykonane w kolejności podanej przez taskArray, począwszy od initialTime.

Algorytm implementujący tę funkcję jest stosunkowo prosty: iteruj po zadaniach w taskArray. Dla każdego zadania, dodać timedo initialTime. Jeśli nowy czas < due, dodaj importancedo akumulatora. Czas jest regulowany przez odwrotną pracęPerDay. Przed zwróceniem akumulatora podziel przez sumę ważności zadań w celu normalizacji.

function weightedTasksOnTime(workPerDay, initialTime, taskArray) {
    let simulatedTime = initialTime
    let accumulator = 0;
    for (task in taskArray) {
        simulatedTime += task.time * (24 / workPerDay)
        if (simulatedTime < task.due) {
            accumulator += task.importance
        }
    }
    return accumulator / totalImportance(taskArray)
}

Wierzę, że powyższy problem można uprościć, zachowując jego rdzeń, usuwając workPerDayi wymagając normalizacji, aby dać:

function weightedTasksOnTime(initialTime, taskArray) {
    let simulatedTime = initialTime
    let accumulator = 0;
    for (task in taskArray) {
        simulatedTime += task.time
        if (simulatedTime < task.due) {
            accumulator += task.importance
        }
    }
    return accumulator
}

To pytanie dotyczy sytuacji, w których testowany kod nie jest ponowną implementacją istniejącego algorytmu. Jeśli kod jest ponowną implementacją, z natury ma on łatwe do przewidzenia wyniki, ponieważ istniejące zaufane implementacje algorytmu działają jak naturalna wyrocznia testowa.

PaintingInAir
źródło
4
Czy możesz podać prosty przykład funkcji, której wynik trudno przewidzieć?
Robert Harvey,
62
FWIW nie testujesz algorytmu. Przypuszczalnie to jest poprawne. Testujesz wdrożenie. Ćwiczenie ręczne jest często dobre jako równoległa konstrukcja.
Kristian H
7
Istnieją sytuacje, w których algorytm nie może być w uzasadniony sposób przetestowany jednostkowo - na przykład, jeśli jego czas realizacji wynosi wiele dni / miesięcy. Może się to zdarzyć podczas rozwiązywania problemów NP. W takich przypadkach może być bardziej prawdopodobne przedstawienie formalnego dowodu, że kod jest poprawny.
Hulk,
12
Coś, co widziałem w bardzo trudnym kodzie numerycznym, polega na traktowaniu testów jednostkowych tylko jako testów regresyjnych. Napisz funkcję, uruchom ją dla kilku interesujących wartości, ręcznie sprawdź poprawność wyników, a następnie napisz test jednostkowy, aby wyłapać regresje od oczekiwanego wyniku. Kodowanie horroru? Ciekawe, co myślą inni.
Chuu,

Odpowiedzi:

251

Istnieją dwie rzeczy, które można przetestować w trudnym do przetestowania kodzie. Po pierwsze, zdegenerowane przypadki. Co się stanie, jeśli nie masz żadnych elementów w tablicy zadań lub tylko jeden lub dwa, ale jeden przekroczył termin realizacji itp. Wszystko, co jest prostsze niż twój prawdziwy problem, ale nadal uzasadnione do obliczenia ręcznego.

Drugi to kontrole poczytalności. Są to kontrole, które wykonujesz, gdy nie wiesz, czy odpowiedź jest prawidłowa , ale na pewno byś wiedział, czy jest zła . Są to takie rzeczy, jak czas musi iść do przodu, wartości muszą mieścić się w rozsądnym zakresie, wartości procentowe muszą się sumować do 100 itd.

Tak, nie jest to tak dobre jak pełny test, ale zdziwiłbyś się, jak często psujesz się przy sprawdzaniu czystości i zdegenerowanych przypadkach, co ujawnia problem w twoim pełnym algorytmie.

Karl Bielefeldt
źródło
54
Myślę, że to bardzo dobra rada. Zacznij od napisania tego rodzaju testów jednostkowych. Podczas opracowywania oprogramowania, jeśli znajdziesz błędy lub nieprawidłowe odpowiedzi - dodaj je jako testy jednostkowe. Zrób to samo, do pewnego stopnia, gdy znajdziesz zdecydowanie poprawne odpowiedzi. Zbuduj je z czasem, a Ty (ostatecznie) będziesz mieć bardzo kompletny zestaw testów jednostkowych, mimo że nie wiesz, co zamierzają ...
Algy Taylor,
21
Inną rzeczą, która może być pomocna w niektórych przypadkach (choć może nie ta), jest napisanie funkcji odwrotnej i przetestowanie, że po połączeniu łańcuchowym dane wejściowe i wyjściowe są takie same.
Cyberspark
7
kontrola poczytalności często stanowi dobry cel dla testów opartych na właściwościach za pomocą czegoś takiego jak QuickCheck
jk.
10
jedną inną kategorią testów, którą polecam, jest kilka, aby sprawdzić, czy nie wystąpiły niezamierzone zmiany w danych wyjściowych. Możesz je oszukiwać, używając samego kodu do generowania oczekiwanego wyniku, ponieważ ich celem jest pomoc opiekunom poprzez oznaczenie, że coś, co miało na celu neutralną zmianę wyjściową, nieumyślnie wpłynęło na zachowanie algorytmu.
Dan Neely,
5
@ iFlo Nie jestem pewien, czy żartujesz, ale odwrotna odwrotność już istnieje. Warto
zdać
80

Pisałem testy oprogramowania naukowego o trudnych do przewidzenia wynikach. Często korzystaliśmy z relacji metamorficznych. Zasadniczo istnieją rzeczy, które wiesz o tym, jak powinno się zachowywać twoje oprogramowanie, nawet jeśli nie znasz dokładnych danych liczbowych.

Możliwy przykład twojej sprawy: jeśli zmniejszysz ilość pracy, którą możesz wykonać każdego dnia, łączna ilość pracy, którą możesz wykonać, pozostanie co najwyżej taka sama, ale prawdopodobnie zmniejszy się. Więc uruchom funkcję dla szeregu wartości workPerDayi upewnij się, że relacja jest zachowana.

James Elderfield
źródło
32
Relacje metamorficzne to szczególny przykład testów opartych na właściwościach , które są ogólnie przydatnym narzędziem w takich sytuacjach
Dannnno,
38

Inne odpowiedzi mają dobre pomysły na opracowanie testów dla przypadku krawędzi lub błędu. Dla pozostałych użycie samego algorytmu nie jest idealne (oczywiście), ale wciąż przydatne.

Wykryje, czy algorytm (lub dane, od których zależy) zmienił się

Jeśli zmiana jest wypadkiem, możesz cofnąć zatwierdzenie. Jeśli zmiana była zamierzona, musisz ponownie sprawdzić test jednostkowy.

użytkownik949300
źródło
6
Dla przypomnienia, tego rodzaju testy są często nazywane „testami regresji” zgodnie z ich przeznaczeniem i są w zasadzie siatką bezpieczeństwa dla wszelkich modyfikacji / refaktoryzacji.
Pac0,
21

W ten sam sposób piszesz testy jednostkowe dla dowolnego innego rodzaju kodu:

  1. Znajdź reprezentatywne przypadki testowe i przetestuj je.
  2. Znajdź przypadki skrajne i przetestuj je.
  3. Znajdź warunki błędu i przetestuj je.

O ile kod nie zawiera losowego elementu lub nie jest deterministyczny (tj. Nie będzie generował tego samego wyniku przy tych samych danych wejściowych), można go testować jednostkowo.

Unikaj skutków ubocznych lub funkcji, na które wpływ mają siły zewnętrzne. Czyste funkcje są łatwiejsze do przetestowania.

Robert Harvey
źródło
2
W przypadku algorytmów niedeterministycznych można zapisać ziarno RNG lub wykpić go za pomocą serii deterministycznych o ustalonej sekwencji lub niskiej rozbieżności, np. Sekwencji Halton
wondra
14
@PaintingInAir Jeśli nie można zweryfikować wyniku algorytmu, czy algorytm może być nawet niepoprawny?
WolfgangGroiss,
5
Unless your code involves some random elementSztuczka polega na tym, aby generator liczb losowych był wstrzykiwaną zależnością, dzięki czemu można go zastąpić generatorem liczb, który daje dokładnie pożądany wynik. Umożliwia to ponowne dokładne przetestowanie - zliczanie wygenerowanych liczb jako parametrów wejściowych. not deterministic (i.e. it won't produce the same output given the same input)Ponieważ test jednostkowy powinien rozpoczynać się od kontrolowanej sytuacji, może być niedeterministyczny tylko wtedy, gdy zawiera element losowy - który można następnie wstrzyknąć. Nie mogę tutaj wymyślić innych możliwości.
Flater,
3
@PaintingInAir: Albo albo. Mój komentarz dotyczy zarówno szybkiego wykonania, jak i szybkiego pisania testu. Jeśli ręczne obliczenie pojedynczego przykładu zajmuje trzy dni (załóżmy, że używasz najszybszej dostępnej metody, która nie korzysta z kodu) - to trzy dni. Jeśli zamiast tego oparłeś oczekiwany wynik testu na samym kodzie, test sam się kompromituje. To jak robienie if(x == x), to bezcelowe porównanie. Potrzebujesz dwóch wyników ( rzeczywistych : pochodzi z kodu; oczekuje się : pochodzi z twojej wiedzy zewnętrznej), aby być niezależnymi od siebie.
Flater
2
Nadal można go testować jednostkowo, nawet jeśli nie jest deterministyczny, pod warunkiem, że jest zgodny ze specyfikacjami i że można zmierzyć zgodność (np. Rozkład i rozkład losowy). Może to wymagać bardzo wielu próbek w celu wyeliminowania ryzyka anomalii.
mckenzm,
17

Aktualizacja z powodu opublikowanych komentarzy

Oryginalna odpowiedź została usunięta ze względu na zwięzłość - można ją znaleźć w historii edycji.

PaintingInAir Dla kontekstu: jako przedsiębiorca i naukowiec większość projektowanych algorytmów nie jest wymagana przez nikogo innego niż ja. Przykład podany w pytaniu jest częścią optymalizatora niezawierającego pochodnych w celu zmaksymalizowania jakości zamówienia zadań. Jeśli chodzi o sposób, w jaki opisałem wewnętrznie potrzebę zastosowania przykładowej funkcji: „Potrzebuję funkcji celu, aby zmaksymalizować znaczenie zadań, które są wykonywane na czas”. Jednak nadal wydaje się, że istnieje duża luka między tym żądaniem a wdrożeniem testów jednostkowych.

Po pierwsze, TL; DR, aby uniknąć długiej odpowiedzi:

Pomyśl o tym w ten sposób:
klient wchodzi do McDonalda i prosi o burgera z sałatą, pomidorem i mydłem do rąk jako dodatkami. To zamówienie jest wydawane kucharzowi, który robi burgera dokładnie zgodnie z żądaniem. Klient otrzymuje tego burgera, je, a następnie skarży się kucharzowi, że nie jest to smaczny burger!

To nie jest wina kucharza - robi tylko to, o co wyraźnie poprosił klient. Zadaniem kucharza nie jest sprawdzenie, czy zamówione zamówienie jest naprawdę smaczne . Kucharz po prostu tworzy to, co klient zamawia. Klient jest odpowiedzialny za zamówienie czegoś, co uzna za smaczne .

Podobnie, zadaniem programisty nie jest kwestionowanie poprawności algorytmu. Ich jedynym zadaniem jest wdrożenie algorytmu zgodnie z żądaniem.
Testy jednostkowe to narzędzie programistyczne. Potwierdza, że ​​burger pasuje do zamówienia (zanim opuści kuchnię). Nie próbuje (i nie powinien) potwierdzać, że zamówiony burger jest naprawdę smaczny.

Nawet jeśli jesteś zarówno klientem, jak i kucharzem, nadal istnieje znaczące rozróżnienie między:

  • Nie przygotowałem właściwie tego posiłku, nie był smaczny (= błąd gotowania). Spalony stek nigdy nie będzie dobrze smakował, nawet jeśli lubisz stek.
  • Przygotowałem posiłek właściwie, ale mi się nie podoba (= błąd klienta). Jeśli nie lubisz steku, nigdy nie spodoba ci się jeść stek, nawet jeśli ugotowałeś go do perfekcji.

Głównym problemem tutaj jest to, że nie rozdzielasz klienta od programisty (i analityka - choć tę rolę może również reprezentować programista).

Musisz rozróżnić między testowaniem kodu a testowaniem wymagań biznesowych.

Na przykład klient chce, aby działał tak [ten] . Jednak deweloper nie rozumie, a on pisze kod, który robi [to] .

Deweloper napisze zatem testy jednostkowe, które sprawdzają, czy [to] działa zgodnie z oczekiwaniami. Jeśli poprawnie opracował aplikację, jego testy jednostkowe zakończą się pomyślnie, nawet jeśli aplikacja nie zrobi tego [tego] , czego oczekiwał klient.

Jeśli chcesz przetestować oczekiwania klienta (wymagania biznesowe), musisz to zrobić w osobnym (i późniejszym) kroku.

Prosty przepływ pracy programistycznej, aby pokazać, kiedy należy uruchomić te testy:

  • Klient wyjaśnia problem, który chce rozwiązać.
  • Analityk (lub programista) zapisuje to w analizie.
  • Deweloper pisze kod, który robi to, co opisuje analiza.
  • Deweloper testuje swój kod (testy jednostkowe), aby sprawdzić, czy poprawnie wykonał analizę
  • Jeśli testy jednostkowe nie powiodą się, programista powraca do programowania. Zapętla się w nieskończoność, dopóki testy jednostki nie przejdą pomyślnie.
  • Mając teraz przetestowaną (potwierdzoną i przekazaną) bazę kodu, programista tworzy aplikację.
  • Aplikacja jest przekazywana klientowi.
  • Klient sprawdza teraz , czy otrzymana przez niego aplikacja faktycznie rozwiązuje problem, który starał się rozwiązać (testy jakości) .

Można się zastanawiać, o co chodzi w przeprowadzaniu dwóch osobnych testów, gdy klient i programista są jednym i tym samym. Ponieważ nie ma „przekazania” od dewelopera do klienta, testy są przeprowadzane jeden po drugim, ale nadal są to osobne kroki.

  • Testy jednostkowe to specjalistyczne narzędzie, które pomaga zweryfikować, czy etap programowania został zakończony.
  • Testy QA są wykonywane przy użyciu aplikacji .

Jeśli chcesz sprawdzić, czy sam algorytm jest poprawny, nie jest to częścią zadania programisty . To troska klienta, a klient przetestuje to za pomocą aplikacji.

Jako przedsiębiorca i pracownik akademicki możesz nie zauważyć tutaj ważnego rozróżnienia, które podkreśla różne obowiązki.

  • Jeśli aplikacja nie spełnia wymagań podanych przez klienta, późniejsze zmiany w kodzie są zwykle wykonywane bezpłatnie ; ponieważ jest to błąd programisty. Deweloper popełnił błąd i musi zapłacić za jego naprawienie.
  • Jeśli aplikacja robi to, o co początkowo prosił klient, ale klient zmienił zdanie (np. Zdecydowałeś się użyć innego i lepszego algorytmu), zmiany w podstawie kodu obciążają klienta , ponieważ nie jest to wina dewelopera, że ​​klient poprosił o coś innego niż teraz chce. Klient jest odpowiedzialny (koszt) za zmianę zdania i dlatego prosi deweloperów o więcej wysiłku, aby opracować coś, czego wcześniej nie uzgodniono.
Flater
źródło
Byłbym szczęśliwy widząc więcej szczegółów na temat sytuacji „Jeśli sam wymyśliłeś algorytm”, ponieważ uważam, że jest to sytuacja, która najprawdopodobniej spowoduje problemy. Zwłaszcza w sytuacjach, gdy nie podano przykładów „jeśli A to B, w przeciwnym razie C”. (ps Nie jestem zwycięzcą)
PaintingInAir
@PaintingInAir: Ale tak naprawdę nie mogę rozwinąć tej kwestii, ponieważ zależy to od twojej sytuacji. Jeśli zdecydowałeś się stworzyć ten algorytm, oczywiście zrobiłeś to, aby zapewnić określoną funkcję. Kto cię o to poprosił? Jak opisali swoją prośbę? Czy powiedzieli ci, co musieli się wydarzyć w niektórych scenariuszach? (informacje te nazywam „analizą” w mojej odpowiedzi). Wszelkie otrzymane wyjaśnienia (które doprowadziły cię do stworzenia algorytmu) można wykorzystać do przetestowania, czy algorytm działa zgodnie z żądaniem. Krótko mówiąc, można użyć wszystkiego oprócz kodu / samodzielnie utworzonego algorytmu .
Flater
2
@PaintingInAir: Ścisłe łączenie klienta, analityka i programisty jest niebezpieczne; ponieważ masz skłonność do pomijania niezbędnych kroków, takich jak zdefiniowanie problemu . Wierzę, że to właśnie tutaj robisz. Wygląda na to, że chcesz przetestować poprawność algorytmu, a nie to, czy został on poprawnie zaimplementowany. Ale nie tak to robisz. Testowanie implementacji można wykonać za pomocą testów jednostkowych. Testowanie samego algorytmu polega na użyciu (przetestowanej) aplikacji i sprawdzeniu jej faktów - ten rzeczywisty test wykracza poza zakres bazy kodu (tak jak powinien ).
Flater,
4
Ta odpowiedź jest już ogromna. Zdecydowanie zalecamy znalezienie sposobu na przeformułowanie oryginalnej treści, aby po prostu zintegrować ją z nową odpowiedzią, jeśli nie chcesz jej wyrzucić.
jpmc26,
7
Nie zgadzam się również z twoją przesłanką. Testy mogą i absolutnie powinny ujawnić, kiedy kod generuje niepoprawne dane wyjściowe zgodnie ze specyfikacją. Jest ważny dla testów, aby sprawdzić poprawność wyników dla niektórych znanych przypadków testowych. Ponadto kucharz powinien wiedzieć lepiej niż akceptować „mydło do rąk” jako ważny składnik hamburgera, a pracodawca prawie na pewno nauczył go, jakie składniki są dostępne.
jpmc26,
9

Testowanie nieruchomości

Czasami funkcje matematyczne są lepiej obsługiwane przez „testowanie właściwości” niż przez tradycyjne testowanie jednostkowe oparte na przykładach. Wyobraź sobie na przykład, że piszesz testy jednostkowe dla czegoś takiego jak funkcja „mnożenia” liczb całkowitych. Chociaż sama funkcja może wydawać się bardzo prosta, jeśli jest to jedyny sposób na pomnożenie, jak ją dokładnie przetestować bez logiki w samej funkcji? Możesz użyć gigantycznych tabel z oczekiwanymi wejściami / wyjściami, ale jest to ograniczone i podatne na błędy.

W takich przypadkach można przetestować znane właściwości funkcji, zamiast szukać konkretnych oczekiwanych wyników. W przypadku mnożenia możesz wiedzieć, że pomnożenie liczby ujemnej i liczby dodatniej powinno skutkować liczbą ujemną oraz że pomnożenie dwóch liczb ujemnych powinno dać liczbę dodatnią itp. Korzystanie z losowych wartości, a następnie sprawdzenie, czy te właściwości są zachowane dla wszystkich wartości testowe to dobry sposób na przetestowanie takich funkcji. Zasadniczo musisz przetestować więcej niż jedną właściwość, ale często możesz zidentyfikować skończony zestaw właściwości, które razem sprawdzają poprawność działania funkcji, niekoniecznie znając oczekiwany wynik dla każdego przypadku.

Jednym z najlepszych wprowadzeń do testowania własności, które widziałem, jest ten w F #. Mam nadzieję, że składnia nie jest przeszkodą w zrozumieniu wyjaśnienia techniki.

Aaron M. Eshbach
źródło
1
Sugerowałbym może dodanie czegoś bardziej szczegółowego w twoim powtórzeniu przykładowym, na przykład generowanie losowych kwartetów (a, b, c) i potwierdzenie, że (ab) (cd) daje (ac-ad) - (bc-bd). Operacja mnożenia może być dość zepsuta i nadal utrzymywać zasadę (ujemny czas ujemny daje dodatnią), ale reguła rozdzielająca przewiduje określone wyniki.
supercat
4

Kuszące jest napisanie kodu, a następnie sprawdzenie, czy wynik „wygląda dobrze”, ale, jak słusznie intuicyjnie, nie jest to dobry pomysł.

Gdy algorytm jest trudny, możesz zrobić wiele rzeczy, aby ułatwić ręczne obliczenie wyniku.

  1. Użyj Excela. Skonfiguruj arkusz kalkulacyjny, który wykonuje niektóre lub wszystkie obliczenia za Ciebie. Uprość to na tyle, aby można było zobaczyć kroki.

  2. Podziel metodę na mniejsze, testowalne, każda z własnymi testami. Gdy masz pewność, że mniejsze części działają, użyj ich do ręcznej pracy przez następny krok.

  3. Użyj właściwości agregujących do sprawdzenia poprawności. Załóżmy na przykład, że masz kalkulator prawdopodobieństwa; możesz nie wiedzieć, jakie powinny być poszczególne wyniki, ale wiesz, że wszystkie muszą się sumować do 100%.

  4. Brutalna siła. Napisz program, który generuje wszystkie możliwe wyniki, i sprawdź, czy żaden z nich nie jest lepszy od generowanego przez algorytm.

Ewan
źródło
W przypadku wersji 3. uwzględnij tutaj niektóre błędy zaokrąglania. Możliwe, że Twoja łączna kwota wynosi 100,000001% lub podobnie zbliżone, ale nie dokładne liczby.
Flater
2
Nie jestem całkiem pewien co do 4. Jeśli jesteś w stanie wygenerować optymalny wynik dla wszystkich możliwych kombinacji danych wejściowych (których następnie używasz do potwierdzenia testu), to z natury jesteś już w stanie obliczyć optymalny wynik i dlatego nie „ potrzebujesz drugiego fragmentu kodu, który próbujesz przetestować. W tym momencie lepiej byłoby użyć istniejącego generatora optymalnych wyników, ponieważ już udowodniono, że działa. (a jeśli nie zostało to jeszcze udowodnione, nie można polegać na wynikach sprawdzania faktów na początku testów).
Flater
6
@flater zwykle masz inne wymagania, a także poprawność, której brutalna siła nie spełnia. np. wydajność.
Ewan,
1
@flater Nienawidzę używać twojego rodzaju, najkrótszej ścieżki, silnika szachowego itp., jeśli w to wierzysz. Ale całkowicie ryzykuję błąd w zaokrąglaniu dozwolony w kasynie przez cały dzień
Ewan
3
@flater zrezygnowałeś po przejściu do gry z pionkiem króla? tylko dlatego, że cała gra nie może być brutalna, nie oznacza indywidualnej pozycji. To, że brutalnie wymuszasz najkrótszą ścieżkę do jednej sieci, nie oznacza, że ​​znasz najkrótszą ścieżkę we wszystkich sieciach
Ewan,
2

TL; DR

Przejdź do sekcji „testy porównawcze”, aby uzyskać porady, których nie ma w innych odpowiedziach.


Początki

Zacznij od przetestowania przypadków, które powinny zostać odrzucone przez algorytm ( workPerDayna przykład zero lub ujemne ) oraz spraw, które są trywialne (np. Pusta taskstablica).

Następnie najpierw przetestuj najprostsze przypadki. W przypadku tasksdanych wejściowych musimy przetestować różne długości; powinno wystarczyć przetestowanie 0, 1 i 2 elementów (2 należy do kategorii „wielu” dla tego testu).

Jeśli znajdziesz dane, które można obliczyć mentalnie, to dobry początek. Techniką, której czasem używam, jest rozpoczęcie od pożądanego wyniku i powrót (w specyfikacji) do danych wejściowych, które powinny dać ten wynik.

Testy porównawcze

Czasami stosunek wyniku do wejścia nie jest oczywisty, ale istnieje przewidywalna zależność między różnymi wyjściami, gdy jedno wejście jest zmieniane. Jeśli dobrze zrozumiałem przykład, dodanie zadania (bez zmiany innych danych wejściowych) nigdy nie zwiększy proporcji pracy wykonanej na czas, dzięki czemu możemy utworzyć test, który wywołuje funkcję dwukrotnie - raz z dodatkowym zadaniem i raz bez niego - i zapewnia nierówność między tymi dwoma wynikami.

Awarie

Czasami musiałem uciekać się do długiego komentarza pokazującego ręcznie obliczony wynik w krokach odpowiadających specyfikacji (taki komentarz jest zwykle dłuższy niż przypadek testowy). Najgorszy przypadek to konieczność zachowania zgodności z wcześniejszą implementacją w innym języku lub w innym środowisku. Czasami wystarczy po prostu oznaczyć dane testowe czymś takim jak /* derived from v2.6 implementation on ARM system */. To nie jest bardzo satysfakcjonujące, ale może być do zaakceptowania jako test wierności podczas przenoszenia lub jako krótkotrwała kula.

Przypomnienia

Najważniejszą cechą testu jest jego czytelność - jeśli wejścia i wyjścia są nieprzezroczyste dla czytnika, wówczas test ma bardzo niską wartość, ale jeśli czytelnikowi pomaga się zrozumieć relacje między nimi, test służy dwóm celom.

Nie zapomnij użyć odpowiedniego „w przybliżeniu równego” dla niedokładnych wyników (np. Zmiennoprzecinkowe).

Unikaj nadmiernego testowania - dodaj test tylko wtedy, gdy obejmuje coś (np. Wartość graniczną), którego nie osiągają inne testy.

Toby Speight
źródło
2

Nie ma nic specjalnego w tego rodzaju trudnych do przetestowania funkcjach. To samo dotyczy kodu korzystającego z zewnętrznych interfejsów (powiedzmy interfejsu API REST aplikacji innej firmy, która nie jest pod twoją kontrolą i na pewno nie będzie testowana przez pakiet testowy; lub przy użyciu biblioteki innej firmy, jeśli nie masz pewności dokładny bajtowy format zwracanych wartości).

Jest to całkiem poprawne podejście, aby po prostu uruchomić algorytm dla pewnych rozsądnych danych wejściowych, zobaczyć, co robi, upewnić się, że wynik jest poprawny, i zamknąć dane wejściowe i wynik jako przypadek testowy. Możesz to zrobić dla kilku przypadków, a tym samym uzyskać kilka próbek. Spróbuj ustawić parametry wejściowe tak różne, jak to możliwe. W przypadku zewnętrznego wywołania API wykonujesz kilka połączeń z prawdziwym systemem, śledzisz je za pomocą jakiegoś narzędzia, a następnie wyśmiewasz je do testów jednostkowych, aby zobaczyć, jak zareaguje Twój program - co jest równoznaczne z wybraniem kilku uruchamia kod planowania zadania, weryfikując je ręcznie, a następnie zapisując wynik w testach.

Następnie, oczywiście, przynieś przypadkowe przypadki, takie jak (w twoim przykładzie) pusta lista zadań; rzeczy takie jak te.

Zestaw testów może nie być tak świetny, jak metoda, w której można łatwo przewidzieć wyniki; ale wciąż o 100% lepszy niż brak zestawu testów (lub tylko test dymu).

Jeśli twoim problemem jest to, że trudno ci zdecydować, czy wynik jest poprawny, to jest to zupełnie inny problem. Załóżmy na przykład, że masz metodę, która wykrywa, czy dowolna duża liczba jest liczbą pierwszą. Trudno rzucić na nią dowolną liczbą losową, a następnie po prostu „spojrzeć”, jeśli wynik jest prawidłowy (zakładając, że nie można zdecydować o pierwszorzędności w głowie lub na kartce papieru). W tym przypadku naprawdę niewiele można zrobić - trzeba albo poznać znane wyniki (np. Niektóre duże liczby pierwsze), albo wdrożyć funkcjonalność za pomocą innego algorytmu (może nawet innego zespołu - NASA wydaje się lubić that) i mam nadzieję, że jeśli którakolwiek implementacja jest błędna, przynajmniej błąd nie prowadzi do takich samych błędnych wyników.

Jeśli jest to zwykły przypadek, musisz dobrze porozmawiać z inżynierami wymagań. Jeśli nie są w stanie sformułować twoich wymagań w sposób łatwy (lub w ogóle możliwy) do sprawdzenia, to kiedy wiesz, czy jesteś skończony?

AnoE
źródło
2

Inne odpowiedzi są dobre, więc postaram się trafić w niektóre punkty, które dotychczas wspólnie przegapili.

Napisałem (i dokładnie przetestowałem) oprogramowanie do przetwarzania obrazu za pomocą radaru z syntetyczną aperturą (SAR). Ma charakter naukowy / liczbowy (wymaga dużo geometrii, fizyki i matematyki).

Kilka wskazówek (dotyczących ogólnych badań naukowych / numerycznych):

1) Użyj odwrotności. Co się fftz [1,2,3,4,5]? Brak pomysłu. Co jest ifft(fft([1,2,3,4,5]))? Powinien być [1,2,3,4,5](lub blisko niego, mogą pojawić się błędy zmiennoprzecinkowe). To samo dotyczy przypadku 2D.

2) Użyj znanych twierdzeń. Jeśli napiszesz funkcję wyznacznika, może być trudno powiedzieć, czym jest wyznacznik losowej macierzy 100 x 100. Ale wiesz, że wyznacznikiem macierzy tożsamości jest 1, nawet jeśli jest 100 x 100. Wiesz również, że funkcja powinna zwracać 0 na nieodwracalnej macierzy (jak 100 x 100 pełna wszystkich zer).

3) Używaj szorstkich stwierdzeń zamiast dokładnych stwierdzeń. Napisałem kod dla wspomnianego przetwarzania SAR, który zarejestrowałby dwa obrazy, generując punkty wiążące, które tworzą mapowanie między obrazami, a następnie wykonując wypaczenie między nimi, aby je dopasować. Może zarejestrować się na poziomie subpikseli. A priori trudno powiedzieć coś o tym, jak mogłaby wyglądać rejestracja dwóch zdjęć. Jak możesz to przetestować? Rzeczy jak:

EXPECT_TRUE(register(img1, img2).size() < min(img1.size(), img2.size()))

ponieważ możesz zarejestrować się tylko na nakładających się częściach, zarejestrowany obraz musi być mniejszy lub równy twojemu najmniejszemu obrazowi, a także:

scale = 255
EXPECT_PIXEL_EQ_WITH_TOLERANCE(reg(img, img), img, .05*scale)

ponieważ zarejestrowany sam obraz powinien być ZAMKNIĘTY dla siebie, ale możesz napotkać nieco więcej niż błędy zmiennoprzecinkowe z powodu zastosowanego algorytmu, więc po prostu sprawdź, czy każdy piksel mieści się w zakresie +/- 5% zakresu, jaki piksele mogą przyjąć (0-255 to skala szarości, powszechna w przetwarzaniu obrazu). Wynik powinien mieć co najmniej taki sam rozmiar jak wejście.

Możesz nawet po prostu przetestować dym (tzn. Zadzwoń i upewnij się, że się nie zawiesi). Ogólnie rzecz biorąc, ta technika jest lepsza w przypadku większych testów, w których wyniku końcowego nie można (łatwo) obliczyć a priori przed uruchomieniem testu.

4) Użyj LUB ZAPISZ losowy numer początkowy dla swojego RNG.

Działa nie muszą być powtarzalne. Fałszywe jest jednak, że jedynym sposobem na uzyskanie powtarzalnego przebiegu jest dostarczenie określonego ziarna do generatora liczb losowych. Czasami testowanie losowości jest cenne. Widziałem / słyszałem o błędy w kodzie naukowych, które pojawić się w zdegenerowanych przypadkach, które zostały losowo generowanych (w skomplikowanych algorytmów może być trudno zobaczyć, co przypadek zdegenerowany nawet jest). Zamiast zawsze wywoływać funkcję z tym samym ziarnem, wygeneruj losowe ziarno, a następnie użyj tego ziarna i zapisz jego wartość. W ten sposób każde uruchomienie ma inne losowe ziarno, ale jeśli wystąpi awaria, możesz ponownie uruchomić wynik, używając ziarna, które zalogowałeś do debugowania. Właściwie użyłem tego w praktyce i to zmiażdżyło błąd, więc pomyślałem, że wspomnę o tym. To prawda, że ​​stało się to tylko raz i jestem pewien, że nie zawsze warto to robić, więc używaj tej techniki z rozwagą. Losowe z tym samym ziarnem jest jednak zawsze bezpieczne. Wada (w przeciwieństwie do ciągłego korzystania z tego samego materiału siewnego): Musisz zalogować swoje testy. Zaleta: poprawność i nukanie błędów.

Twoja szczególna sprawa

1) Sprawdź, czy pusty taskArray zwraca 0 (znane twierdzenie).

2) Generowanie losowych danych wejściowych tak, że task.time > 0 , task.due > 0, a task.importance > 0 dla wszystkich task s, i twierdzą, wynik jest większy niż 0 (Gorsza Twierdzę wejściowym losowe) . Nie musisz zwariować i wygenerować losowych nasion, twój algorytm po prostu nie jest wystarczająco skomplikowany, aby to uzasadnić. Jest około 0 szans, że się opłaci: po prostu uprość test.

3) Sprawdź, czy task.importance == 0 dla wszystkich task s wynik jest 0 (znany)

4) Dotknęły tego inne odpowiedzi, ale może to mieć znaczenie dla konkretnego przypadku: jeśli tworzysz interfejs API do użytku przez użytkowników spoza zespołu, musisz przetestować zdegenerowane przypadki. Na przykład, jeśli workPerDay == 0zgłaszasz piękny błąd, który informuje użytkownika, że ​​dane wejściowe są nieprawidłowe. Jeśli nie tworzysz interfejsu API i jest on przeznaczony tylko dla Ciebie i Twojego zespołu, prawdopodobnie możesz pominąć ten krok i po prostu odmówić połączenia z degeneracją.

HTH.

Matt Messersmith
źródło
1

Włącz testowanie asercji do pakietu testów jednostkowych w celu testowania algorytmu na podstawie właściwości. Oprócz pisania testów jednostkowych, które sprawdzają określone dane wyjściowe, pisz testy zaprojektowane z myślą o niepowodzeniu przez wyzwalanie błędów asercji w kodzie głównym.

Wiele algorytmów opiera się na dowodach poprawności na zachowaniu określonych właściwości na wszystkich etapach algorytmu. Jeśli możesz rozsądnie sprawdzić te właściwości, patrząc na wynik funkcji, wystarczy testowanie jednostkowe, aby przetestować swoje właściwości. W przeciwnym razie testy oparte na asercjach pozwalają przetestować, czy implementacja zachowuje właściwość za każdym razem, gdy algorytm ją przyjmuje.

Testy oparte na asercji ujawnią wady algorytmu, błędy w kodowaniu i błędy implementacji z powodu takich problemów, jak niestabilność numeryczna. Wiele języków ma mechanizmy usuwania asercji w czasie kompilacji lub przed interpretacją kodu, aby podczas uruchamiania w trybie produkcyjnym asercje nie powodowały pogorszenia wydajności. Jeśli Twój kod przejdzie testy jednostkowe, ale nie powiedzie się w prawdziwej sprawie, możesz włączyć asercje z powrotem jako narzędzie do debugowania.

Tobias Hagge
źródło
1

Niektóre inne odpowiedzi tutaj są bardzo dobre:

  • Przetestuj przypadki baz, krawędzi i narożników
  • Wykonaj kontrolę poczytalności
  • Wykonaj testy porównawcze

... dodałbym kilka innych taktyk:

  • Rozłóż problem.
  • Udowodnij algorytm poza kodem.
  • Sprawdź, czy algorytm [sprawdzony zewnętrznie] jest implementowany zgodnie z projektem.

Dekompozycja pozwala upewnić się, że składniki algorytmu wykonują to, czego się od nich oczekuje. A „dobry” rozkład pozwala również upewnić się, że są odpowiednio sklejone. Wielki rozkład uogólnia i upraszcza algorytm do tego stopnia, że można przewidzieć rezultaty (uproszczonego, rodzajowego algorytmu (ów)) na rękę tyle dobrze, żeby napisać dokładne testy.

Jeśli nie możesz rozłożyć się w takim stopniu, udowodnij, że algorytm poza kodem jest w jakikolwiek sposób wystarczający, aby zadowolić Ciebie i Twoich rówieśników, interesariuszy i klientów. A potem wystarczy rozłożyć tyle, by udowodnić, że twoja implementacja pasuje do projektu.

svidgen
źródło
0

Może to wydawać się idealistyczną odpowiedzią, ale pomaga zidentyfikować różne rodzaje testów.

Jeśli dla implementacji ważne są ścisłe odpowiedzi, wówczas w wymaganiach opisujących algorytm należy podać przykłady i oczekiwane odpowiedzi. Te wymagania powinny zostać przejrzane grupowo, a jeśli nie uzyskasz takich samych wyników, powód musi zostać zidentyfikowany.

Nawet jeśli grasz zarówno jako analityk, jak i implementator, powinieneś stworzyć wymagania i sprawdzić je na długo przed napisaniem testów jednostkowych, więc w takim przypadku będziesz znać oczekiwane wyniki i odpowiednio napisać testy.

Z drugiej strony, jeśli jest to implementowana część, która albo nie jest częścią logiki biznesowej, albo obsługuje odpowiedź logiki biznesowej, to dobrze jest uruchomić test, aby zobaczyć, jakie są wyniki, a następnie zmodyfikować test, aby się spodziewać te wyniki. Wyniki końcowe są już sprawdzane pod kątem wymagań, więc jeśli są poprawne, cały kod zasilający te wyniki końcowe musi być poprawny numerycznie, a w tym momencie testy jednostkowe służą raczej do wykrywania przypadków awarii krawędzi i przyszłych zmian refaktoryzacji niż do udowodnienia, że ​​dane algorytm daje prawidłowe wyniki.

Bill K.
źródło
0

Myślę, że czasami jest całkowicie do przyjęcia proces:

  • zaprojektować skrzynkę testową
  • użyj swojego oprogramowania, aby uzyskać odpowiedź
  • sprawdź odpowiedź ręcznie
  • napisz test regresji, aby przyszłe wersje oprogramowania nadal udzielały tej odpowiedzi.

Jest to rozsądne podejście w każdej sytuacji, w której ręczne sprawdzenie poprawności odpowiedzi jest łatwiejsze niż ręczne obliczenie odpowiedzi na podstawie pierwszych zasad.

Znam ludzi, którzy piszą oprogramowanie do renderowania wydrukowanych stron i mają testy, które sprawdzają, czy na wydrukowanej stronie ustawione są dokładnie odpowiednie piksele. Jedynym rozsądnym sposobem na to jest napisanie kodu do renderowania strony, sprawdzenie wzrokowo, czy wygląda dobrze, a następnie przechwycenie wyniku jako testu regresji dla przyszłych wydań.

Tylko dlatego, że czytasz w książce, że określona metodologia zachęca najpierw do napisania przypadków testowych, nie oznacza, że ​​zawsze musisz to robić w ten sposób. Zasady są do złamania.

Michael Kay
źródło
0

Inne odpowiedzi odpowiedzi już zawierają techniki, jak wygląda test, gdy konkretnego wyniku nie można ustalić poza testowaną funkcją.

To, co robię dodatkowo, czego nie zauważyłem w innych odpowiedziach, to automatyczne generowanie testów w jakiś sposób:

  1. Wejścia „losowe”
  2. Iteracja w różnych zakresach danych
  3. Konstrukcja przypadków testowych z zestawów granic
  4. Wszystko powyższe.

Na przykład, jeśli funkcja przyjmuje trzy parametry o dozwolonym zakresie wejściowym [-1,1], przetestuj wszystkie kombinacje każdego parametru, {-2, -1.01, -1, -0,99, -0,5, -0,01, 0,0,01 , 0,5,0,99,1,1,01,2, niektóre losowe w (-1,1)}

W skrócie: Czasami słaba jakość może być subsydiowana ilościowo.

Keith
źródło