Staram się poradzić sobie z testowaniem jednostkowym.
Załóżmy, że mamy kość, która może mieć domyślną liczbę boków równą 6 (ale może mieć 4, 5 stron itp.):
import random
class Die():
def __init__(self, sides=6):
self._sides = sides
def roll(self):
return random.randint(1, self._sides)
Czy poniższe testy byłyby ważne / przydatne?
- przetestuj rzut w zakresie 1-6 dla kostki 6-stronnej
- przetestuj rzut 0 dla 6-stronnej kostki
- przetestuj rzut 7 dla kostki 6-stronnej
- przetestuj rzut w zakresie 1-3 dla matrycy 3-stronnej
- przetestuj rzut 0 dla 3-stronnej kości
- przetestuj rzut 4 dla 3-stronnej kostki
Po prostu myślę, że to strata czasu, ponieważ moduł losowy istnieje już wystarczająco długo, ale myślę, że jeśli moduł losowy zostanie zaktualizowany (powiedzmy, że aktualizuję wersję Pythona), to przynajmniej jestem objęty.
Czy muszę nawet testować inne odmiany rolek matrycy, np. 3 w tym przypadku, czy też dobrze jest pokryć inny zainicjowany stan matrycy?
python
unit-testing
tdd
Cybran
źródło
źródło
Odpowiedzi:
Masz rację, twoje testy nie powinny sprawdzać, czy
random
moduł wykonuje swoją pracę; unittest powinien testować tylko samą klasę, a nie sposób interakcji z innym kodem (który powinien być testowany osobno).Jest oczywiście całkowicie możliwe, że Twój kod używa
random.randint()
niewłaściwie; lubrandom.randrange(1, self._sides)
zamiast tego dzwonisz, a twoja kość nigdy nie rzuca najwyższej wartości, ale byłby to inny rodzaj błędu, który nie byłby w stanie złapać przy najmniejszym poziomie. W takim przypadkudie
urządzenie działa zgodnie z przeznaczeniem, ale sam projekt był wadliwy.W tym przypadku użyłbym wyśmianie aby wymienić się
randint()
funkcji, a jedynie sprawdzić, czy został on nazywany poprawnie. Python 3.3 i nowsze wersje są dostarczane zunittest.mock
modułem do obsługi tego typu testów, ale można zainstalowaćmock
pakiet zewnętrzny na starszych wersjach, aby uzyskać dokładnie taką samą funkcjonalnośćDzięki drwiom test jest teraz bardzo prosty; tak naprawdę są tylko 2 przypadki. Domyślny przypadek dla 6-stronnej matrycy i niestandardowy przypadek po bokach.
Istnieją inne sposoby tymczasowego zastąpienia
randint()
funkcji w globalnej przestrzeni nazwDie
, alemock
moduł czyni to najłatwiejszym.@mock.patch
Dekorator tutaj odnosi się do wszystkich metod badań w przypadku badania; do każdej metody testowej przekazywany jest dodatkowy argument - wyśmiewanarandom.randint()
funkcja, dzięki czemu możemy testować przeciwko próbce, aby sprawdzić, czy rzeczywiście została poprawnie wywołana. Wreturn_value
Określa argumentów co wrócił z mock kiedy to się nazywa, więc możemy sprawdzić, czydie.roll()
metoda rzeczywiście wrócił „random” rezultat do nas.Użyłem tutaj kolejnej najlepszej praktyki Pythona: zaimportuj testowaną klasę w ramach testu.
_make_one
Metoda działa importowanie i konkretyzacji w teście , tak że test moduł nadal będzie ładować nawet jeśli popełnił błąd składni lub inny błąd, który będzie zapobiegać oryginalny moduł do importu.W ten sposób, jeśli popełnisz błąd w samym kodzie modułu, testy będą nadal uruchamiane; po prostu zawiodą, informując o błędzie w kodzie.
Dla jasności powyższe testy są wyjątkowo uproszczone. Naszym celem nie jest na przykład testowanie
random.randint()
przy użyciu odpowiednich argumentów. Zamiast tego celem jest przetestowanie, czy jednostka daje prawidłowe wyniki przy określonych danych wejściowych, przy czym dane wejściowe obejmują wyniki innych jednostek, które nie są testowane. Wyśmiewającrandom.randint()
metodę, możesz przejąć kontrolę nad kolejnymi danymi wejściowymi do swojego kodu.W rzeczywistych testach rzeczywisty kod w testowanym urządzeniu będzie bardziej złożony; związek z danymi wejściowymi przekazywanymi do interfejsu API i sposób wywoływania innych jednostek może być nadal interesujący, a kpina zapewni dostęp do wyników pośrednich, a także umożliwi ustawienie wartości zwracanych dla tych wywołań.
Na przykład w kodzie, który uwierzytelnia użytkowników na podstawie usługi OAuth2 innej firmy (interakcja wieloetapowa), chcesz przetestować, czy Twój kod przekazuje odpowiednie dane do tej usługi innej firmy i pozwala wyśmiewać różne odpowiedzi na błędy, które Usługa innej firmy powróci, umożliwiając symulację różnych scenariuszy bez konieczności samodzielnego budowania pełnego serwera OAuth2. W tym miejscu ważne jest przetestowanie, czy informacje z pierwszej odpowiedzi zostały poprawnie obsłużone i zostały przekazane do wywołania drugiego etapu, więc chcesz zobaczyć, czy fałszywa usługa jest wywoływana poprawnie.
źródło
randint()
, a nie kod wDie.roll()
.sentinel.die
na przykład (obiekt wartownikaunittest.mock
też), a następnie sprawdź, czy to, co zostało zwrócone z metody rzutu. To faktycznie pozwala tylko na jeden sposób implementacji testowanej metody.sentinel.die
byłby to świetny sposób, aby to zapewnić.Odpowiedź Martijna brzmi: jak byś to zrobił, gdybyś naprawdę chciał przeprowadzić test, który pokazuje, że nazywasz się random.randint. Jednak ryzykując powiedzenie „to nie odpowiada na pytanie”, uważam, że nie powinno to być w ogóle testowane jednostkowo. Szyderczy randint nie jest już testowaniem czarnych skrzynek - konkretnie pokazujesz, że pewne rzeczy dzieją się podczas implementacji . Testowanie czarnej skrzynki nie jest nawet opcją - nie można wykonać testu, który udowodni, że wynik nigdy nie będzie mniejszy niż 1 lub większy niż 6.
Umiesz kpić
randint
? Tak, możesz. Ale co udowadniasz? Że nazwałeś to argumentami 1 i stronami. Co to oznacza? Wróciłeś do punktu wyjścia - pod koniec dnia musisz udowodnić - formalnie lub nieoficjalnie - żerandom.randint(1, sides)
prawidłowe sprawdzenie powoduje rzut kostką.Jestem za testami jednostkowymi. Są to fantastyczne kontrole poczytalności i ujawniają obecność błędów. Jednak nigdy nie mogą udowodnić swojej nieobecności, a są rzeczy, których nie można w ogóle potwierdzić podczas testowania (np. Że określona funkcja nigdy nie zgłasza wyjątku lub zawsze kończy się.) W tym konkretnym przypadku czuję, że niewiele jest do zaoferowania zdobyć. W przypadku deterministycznego zachowania testy jednostkowe mają sens, ponieważ faktycznie wiesz, jakiej odpowiedzi oczekujesz.
źródło
random.randint
wywołanej za pomocą1, sides
jest bezwartościowe, jeśli jest to niewłaściwa rzecz.random.randint()
które poprawnie zwracają wartości z zakresu [1, strony] (włącznie), to zależy od programistów Pythona, aby upewnić się, żerandom
jednostka działa poprawnie.random.randint()
się zachowywać jakrandom.randrange()
i wywoływać gorandom.randint(1, sides + 1)
, to i tak jesteś zatopiony.Napraw losowe ziarno. W przypadku kości 1, 2, 5 i 12-stronnych potwierdź, że kilka tysięcy rzutów daje wyniki, w tym 1 i N, a nie 0 lub N + 1. Jeśli z pozoru dziwna szansa, otrzymasz zestaw losowych wyników, które nie pokrywamy oczekiwany zakres, przełączamy na inne nasiona.
Narzędzia kpienia są fajne, ale to, że pozwalają ci coś zrobić, nie oznacza, że należy to zrobić. YAGNI ma zastosowanie zarówno do urządzeń testowych, jak i do funkcji.
Jeśli możesz łatwo przetestować przy użyciu nieopartych zależności, prawie zawsze powinieneś; w ten sposób twoje testy będą koncentrować się na zmniejszeniu liczby wad, a nie tylko na zwiększeniu liczby testów. Nadmierne kpiny grożą stworzeniem mylących danych dotyczących zasięgu, co z kolei może prowadzić do przełożenia faktycznych testów na późniejszą fazę, której być może nigdy nie zdążycie ...
źródło
Co się stanie,
Die
jeśli o tym pomyślisz? - nie więcej niż opakowanierandom
. To obudowujerandom.randint
i relabels go w kategoriach własnego słownictwa danej aplikacji:Die.Roll
.Nie wydaje mi się istotne, aby wstawiać kolejną warstwę abstrakcji pomiędzy
Die
irandom
ponieważDie
sama jest już tą warstwą pośrednictwa między twoją aplikacją a platformą.Jeśli chcesz wyniki w kostkach z puszki, po prostu kpij
Die
, nie kpijrandom
.Zasadniczo nie testuję jednostkowo moich obiektów opakowania komunikujących się z systemami zewnętrznymi, piszę dla nich testy integracyjne. Możesz napisać kilka takich,
Die
ale jak wskazałeś, ze względu na losowy charakter obiektu leżącego u podstaw, nie będą one miały znaczenia. Ponadto nie ma tu potrzeby konfiguracji ani komunikacji sieciowej, więc nie trzeba wiele testować poza połączeniem z platformą.=> Biorąc pod uwagę, że
Die
jest to tylko kilka trywialnych wierszy kodu i niewiele dodaje żadnej logiki w porównaniu dorandom
samej siebie, pominę testowanie jej w tym konkretnym przykładzie.źródło
Rozsiewanie generatora liczb losowych i weryfikacja oczekiwanych wyników NIE jest, o ile widzę, poprawnym testem. Przyjmuje założenia, w jaki sposób kości działają wewnętrznie, co jest niegrzeczne-niegrzeczne. Twórcy Pythona mogą zmienić generator liczb losowych lub kostkę (UWAGA: „kostki” są w liczbie mnogiej, „kostka” jest pojedyncza. O ile twoja klasa nie wykonuje wielu rzutów kostką w jednym wywołaniu, prawdopodobnie powinna być nazywana „kostką”) użyj innego generatora liczb losowych.
Podobnie, wyśmiewanie funkcji losowej zakłada, że implementacja klasy działa dokładnie zgodnie z oczekiwaniami. Dlaczego może tak nie być? Ktoś może przejąć kontrolę nad domyślnym generatorem liczb losowych w Pythonie i aby tego uniknąć, przyszła wersja twojej kości może pobrać kilka liczb losowych lub większe liczby losowe, aby zmieszać więcej danych losowych. Podobny schemat zastosowali twórcy systemu operacyjnego FreeBSD, gdy podejrzewali, że NSA manipuluje sprzętowymi generatorami liczb losowych wbudowanymi w procesory.
Gdybym to był ja, pobiegłbym, powiedzmy, 6000 rolek, zliczając je i upewniając się, że każda liczba od 1-6 jest wyrzucana między 500 a 1500 razy. Sprawdziłbym również, czy nie są zwracane liczby spoza tego zakresu. Mogę również sprawdzić, czy dla drugiego zestawu 6000 rolek przy zamówieniu [1..6] w kolejności częstotliwości wynik jest inny (nie powiedzie się to raz na 720 uruchomień, jeśli liczby są losowe!). Jeśli chcesz być dokładny, możesz znaleźć częstotliwość liczb po 1, po 2 itd. ale upewnij się, że rozmiar próbki jest wystarczająco duży i masz wystarczającą wariancję. Ludzie oczekują, że liczby losowe będą miały mniej wzorów niż w rzeczywistości.
Powtórz dla matrycy 12-stronnej i 2-stronnej (najczęściej używana jest 6, więc jest najbardziej oczekiwana dla każdego, kto pisze ten kod).
Na koniec chciałbym przetestować, co się dzieje z jednostronną matrycą, matrycą 0-stronną, matrycą 1-stronną, matrycą 2,3-stronną, matrycą [1,2,3,4,5,6], oraz jednostronna śmierć. Oczywiście wszystko to powinno zawieść; czy zawodzą w pożyteczny sposób? Powinny one prawdopodobnie zawieść przy tworzeniu, a nie przy toczeniu.
A może chcesz też traktować je inaczej - być może tworzenie kości z [1,2,3,4,5,6] powinno być dopuszczalne - a być może „bla”; może to być kość z 4 twarzami, a każda twarz ma na sobie literę. Przychodzi na myśl gra „Boggle”, podobnie jak magiczna ósemka.
I na koniec warto rozważyć: http://lh6.ggpht.com/-fAGXwbJbYRM/UJA_31ACOLI/AAAAAAAAAAPg/2FxOWzo96KE/s1600-h/random%25255B3%25255D.jpg
źródło
Ryzykując pływanie pod prąd, rozwiązałem ten dokładny problem wiele lat temu, stosując metodę, o której jeszcze nie wspomniano.
Moją strategią było po prostu wyśmiewanie RNG za pomocą takiego, który wytwarza przewidywalny strumień wartości obejmujący całą przestrzeń. Jeśli (powiedzmy) strona = 6, a RNG generuje wartości od 0 do 5 w sekwencji, mogę przewidzieć, jak powinna się zachowywać moja klasa i odpowiednio przetestować jednostkę.
Uzasadnieniem jest to, że testuje to logikę tylko w tej klasie, przy założeniu, że RNG ostatecznie wytworzy każdą z tych wartości i bez testowania samej RNG.
Jest prosty, deterministyczny, powtarzalny i łapie błędy. Użyłbym tej samej strategii ponownie.
Pytanie nie precyzuje, jakie powinny być testy, tylko jakie dane mogą być użyte do testowania, biorąc pod uwagę obecność RNG. Moją sugestią jest jedynie wyczerpujące przetestowanie kpiny z RNG. Pytanie o to, co warto przetestować, zależy od informacji, których nie podano w pytaniu.
źródło
Testy, które sugerujesz w swoim pytaniu, nie wykrywają modułowego licznika arytmetycznego jako implementacji. I nie wykrywają typowych błędów implementacyjnych w kodzie związanym z rozkładem prawdopodobieństwa
return 1 + (random.randint(1,maxint) % sides)
. Lub zmiana generatora, która powoduje powstanie dwuwymiarowych wzorów.Jeśli naprawdę chcesz sprawdzić, czy generujesz równomiernie rozmieszczone losowo wyglądające liczby, musisz sprawdzić bardzo szeroką gamę właściwości. Aby wykonać dość dobrą robotę, możesz uruchomić http://www.phy.duke.edu/~rgb/General/dieharder.php na wygenerowanych liczbach. Lub napisz podobnie złożony zestaw testów jednostkowych.
To nie wina testów jednostkowych ani TDD, przypadkowość jest po prostu bardzo trudną do zweryfikowania właściwością. I popularny temat przykładów.
źródło
Najłatwiejszym testem rzutu jest powtórzenie go kilkaset tysięcy razy i sprawdzenie, czy każdy możliwy wynik trafił z grubsza (1 / liczbę stron) razy. W przypadku 6-stronnej kości powinieneś zobaczyć każdą możliwą wartość uderzoną przez około 16,6% czasu. Jeśli jakieś są wyłączone o więcej niż jeden procent, masz problem.
Robiąc to w ten sposób, unikasz możliwości refaktoryzacji podstawowej mechaniki generowania liczb losowych łatwo, a co najważniejsze, bez zmiany testu.
źródło