Załóżmy, że piszesz grę TDD w stylu Yahtzee. Chcesz przetestować tę część kodu, która określa, czy zestaw pięciu rzutów matryc jest fularem, czy nie. O ile mi wiadomo, wykonując TDD, przestrzegasz następujących zasad:
- Najpierw napisz testy
- Napisz najprostszą możliwą rzecz, która działa
- Udoskonal i refaktoryzuj
Tak więc początkowy test może wyglądać mniej więcej tak:
public void Returns_true_when_roll_is_full_house()
{
FullHouseTester sut = new FullHouseTester();
var actual = sut.IsFullHouse(1, 1, 1, 2, 2);
Assert.IsTrue(actual);
}
Postępując zgodnie z „Napisz najprostszą możliwą rzeczą, która działa”, powinieneś teraz napisać następującą IsFullHouse
metodę:
public bool IsFullHouse(int roll1, int roll2, int roll3, int roll4, int roll5)
{
if (roll1 == 1 && roll2 == 1 && roll3 == 1 && roll4 == 2 && roll5 == 2)
{
return true;
}
return false;
}
Powoduje to zielony test, ale wdrożenie jest niekompletne.
Czy powinieneś przetestować każdą możliwą prawidłową kombinację (zarówno wartości, jak i pozycji) dla fula? Wygląda to na jedyny sposób, aby mieć absolutną pewność, że Twój IsFullHouse
kod został całkowicie przetestowany i poprawny, ale wydaje się to szalone.
Jak byś przetestował coś takiego?
Aktualizacja
Erik i Kilian podkreślają, że użycie literałów w początkowej implementacji w celu uzyskania zielonego testu może nie być najlepszym pomysłem. Chciałbym wyjaśnić, dlaczego to zrobiłem, a to wyjaśnienie nie pasuje do komentarza.
Moje praktyczne doświadczenie w testowaniu jednostkowym (szczególnie przy zastosowaniu metody TDD) jest bardzo ograniczone. Pamiętam, jak oglądałem nagranie TDD Masterclass Roy'a Osherove na Tekpub. W jednym z odcinków buduje styl TDD Kalkulatora Ciągów. Pełną specyfikację kalkulatora ciągów można znaleźć tutaj: http://osherove.com/tdd-kata-1/
Zaczyna od takiego testu:
public void Add_with_empty_string_should_return_zero()
{
StringCalculator sut = new StringCalculator();
int result = sut.Add("");
Assert.AreEqual(0, result);
}
Powoduje to pierwszą implementację Add
metody:
public int Add(string input)
{
return 0;
}
Następnie dodaje się ten test:
public void Add_with_one_number_string_should_return_number()
{
StringCalculator sut = new StringCalculator();
int result = sut.Add("1");
Assert.AreEqual(1, result);
}
A Add
metoda jest refactored:
public int Add(string input)
{
if (input.Length == 0)
{
return 0;
}
return 1;
}
Po każdym kroku Roy mówi „Napisz najprostszą rzecz, która zadziała”.
Pomyślałem więc, że spróbuję tego podejścia, próbując zrobić grę Yahtzee w stylu TDD.
źródło
if (roll1 == 1 && roll2 == 1 && roll3 == 1 && roll4 == 2 && roll5 == 2)
Odpowiedzi:
Istnieje już wiele dobrych odpowiedzi na to pytanie, a kilka z nich skomentowałem i poprawiłem. Mimo to chciałbym dodać kilka przemyśleń.
Elastyczność nie jest dla nowicjuszy
OP wyraźnie stwierdza, że nie ma doświadczenia z TDD i myślę, że dobra odpowiedź musi to wziąć pod uwagę. W terminologii modelu nabywania umiejętności Dreyfusa jest prawdopodobnie nowicjuszem . Nie ma nic złego w byciu nowicjuszem - wszyscy jesteśmy nowicjuszami, kiedy zaczynamy uczyć się czegoś nowego. Jednak model Dreyfusa wyjaśnia, że charakteryzują się nowicjusze
To nie jest opis niedoboru osobowości, więc nie ma się czego wstydzić - to etap, przez który wszyscy musimy przejść, aby nauczyć się czegoś nowego.
Dotyczy to również TDD.
Chociaż zgadzam się z wieloma innymi odpowiedziami tutaj, że TDD nie musi być dogmatyczne i że czasami praca w alternatywny sposób może być bardziej korzystna, ale to nie pomaga nikomu dopiero zaczynać. Jak możesz zastosować uznaniowy osąd, jeśli nie masz doświadczenia?
Jeśli nowicjusz zaakceptuje radę, że czasami nie jest dozwolone stosowanie TDD, jak może ustalić, kiedy można pominąć TDD?
Bez doświadczenia ani wskazówek jedyne, co może zrobić początkujący, to pominąć TDD za każdym razem, gdy staje się to zbyt trudne. To ludzka natura, ale nie jest to dobry sposób na naukę.
Posłuchaj testów
Pomijanie TDD w dowolnym momencie staje się utratą jednej z najważniejszych zalet TDD. Testy dostarczają wczesnych informacji zwrotnych na temat interfejsu API SUT. Jeśli test jest trudny do napisania, jest to ważny znak, że SUT jest trudny w użyciu.
Oto jeden z najważniejszych komunikatów GOOS : słuchaj swoich testów!
W przypadku tego pytania moją pierwszą reakcją, kiedy zobaczyłem proponowane API gry Yahtzee, i dyskusję na temat kombinatoryki, którą można znaleźć na tej stronie, było to, że jest to ważna opinia na temat API.
Czy API musi reprezentować rzuty kostką jako uporządkowaną sekwencję liczb całkowitych? Dla mnie ten zapach Prymitywnej Obsesji . Dlatego cieszyłem się, widząc odpowiedź z wysokości sugerującą wprowadzenie
Roll
klasy. Myślę, że to doskonała sugestia.Myślę jednak, że niektóre komentarze do tej odpowiedzi są błędne. To, co sugeruje TDD, to to, że kiedy
Roll
wpadniesz na pomysł, że klasa byłaby dobrym pomysłem, zawiesisz pracę nad oryginalnym SUT i zaczniesz pracę nad TDD wRoll
klasie.Chociaż zgadzam się, że TDD jest bardziej ukierunkowane na „szczęśliwą ścieżkę” niż na kompleksowe testy, nadal pomaga rozbić system na możliwe do zarządzania jednostki. A
Roll
dźwięki klasy jak coś można znacznie łatwiej TDD do końca.Następnie, gdy
Roll
klasa będzie wystarczająco rozwinięta, wróć do pierwotnego SUT i dopracuj go pod względemRoll
nakładów.Sugestia Pomocnika Testowego niekoniecznie oznacza losowość - to tylko sposób na uczynienie testu bardziej czytelnym.
Innym sposobem podejścia i modelowania danych wejściowych pod względem
Roll
instancji byłoby wprowadzenie Konstruktora danych testowych .Czerwony / zielony / refaktor to proces trzystopniowy
Chociaż zgadzam się z ogólnym sentymentem, że (jeśli masz wystarczające doświadczenie w TDD), nie musisz rygorystycznie trzymać się TDD, myślę, że to dość kiepska rada w przypadku ćwiczenia Yahtzee. Chociaż nie znam szczegółów reguł Yahtzee, nie widzę tutaj przekonującego argumentu, że nie można rygorystycznie trzymać się procesu Red / Green / Refactor i nadal osiągnąć właściwy wynik.
Większość ludzi tutaj zapomina o trzecim etapie procesu Red / Green / Refactor. Najpierw napisz test. Następnie piszesz najprostszą implementację, która przejdzie wszystkie testy. Potem refaktoryzujesz.
To tutaj, w tym trzecim stanie, możesz wnieść wszystkie swoje umiejętności zawodowe. Tutaj możesz zastanowić się nad kodem.
Jednak myślę, że jest to kłamstwo stwierdzające, że powinieneś tylko „Napisz najprostszą możliwą rzecz, która nie jest całkowicie braindead i oczywiście niepoprawna, która działa”. Jeśli wiesz (wcześniej), że wiesz wystarczająco dużo o implementacji, wtedy wszystko kompletne rozwiązanie będzie oczywiście niepoprawne . Jeśli chodzi o porady, jest to całkiem bezużyteczne dla początkującego.
To, co naprawdę powinno się zdarzyć, to to, że jeśli zdołasz zaliczyć wszystkie testy z oczywiście nieprawidłową implementacją, to powinieneś napisać kolejny test .
Zaskakujące jest to, jak często robienie tego prowadzi do zupełnie innej implementacji niż ta, o której myślałeś wcześniej. Czasami alternatywa, która rośnie w ten sposób, może okazać się lepsza niż twój pierwotny plan.
Rigor to narzędzie do nauki
Sensowne jest trzymanie się rygorystycznych procesów, takich jak czerwony / zielony / refaktor, o ile tylko się uczysz. Zmusza ucznia do zdobywania doświadczenia z TDD nie tylko wtedy, gdy jest to łatwe, ale także gdy jest trudne.
Tylko po opanowaniu wszystkich trudnych części jesteś w stanie podjąć świadomą decyzję, kiedy zejść z „prawdziwej” ścieżki. Wtedy zaczniesz tworzyć własną ścieżkę.
źródło
Jako zrzeczenie się, jest to TDD, gdy go ćwiczę i, jak trafnie zauważył Kilian, uważałbym na każdego, kto zasugerowałby, że istnieje jeden właściwy sposób na jego przećwiczenie. Ale może ci to pomoże ...
Po pierwsze, najprostszą rzeczą, którą możesz zrobić, aby zdać test, jest:
Jest to istotne, ponieważ nie z powodu niektórych praktyk TDD, ale dlatego, że twarde pisanie we wszystkich tych literałach nie jest tak naprawdę dobrym pomysłem. Jedną z najtrudniejszych rzeczy do obejrzenia za pomocą TDD jest to, że nie jest to kompleksowa strategia testowania - to sposób na ochronę przed regresjami i oznaczanie postępu przy jednoczesnym utrzymaniu prostego kodu. To strategia rozwoju , a nie strategia testowania.
Powodem, dla którego wspominam o tym rozróżnieniu, jest to, że pomaga w określeniu, jakie testy powinieneś napisać. Odpowiedź na „jakie testy mam napisać?” to „wszelkie testy potrzebne do uzyskania kodu tak, jak chcesz”. Pomyśl o TDD jako sposobie, w jaki możesz pomóc dokuczać algorytmom i uzasadniać swój kod. Więc biorąc pod uwagę twój test i moją „prostą zieloną” implementację, jaki test będzie następny? Cóż, stworzyłeś coś, co jest domem pełnym, więc kiedy to nie jest dom pełen?
Teraz musisz znaleźć sposób na rozróżnienie dwóch przypadków testowych, które mają znaczenie . Osobiście przydałbym się trochę wyjaśnień na temat „zrób najprostszą rzecz, aby zdać test” i powiem „zrób najprostszą rzecz, aby zdać test, który przyspieszy twoją implementację”. Pisanie testów zakończonych niepowodzeniem jest twoim pretekstem do zmiany kodu, więc kiedy zaczynasz pisać każdy test, powinieneś zadać sobie pytanie: „czego mój kod nie robi tak, jak chcę, i jak mogę go ujawnić?” Może to również pomóc w zwiększeniu odporności kodu i obsłudze przypadków brzegowych. Co robisz, jeśli dzwoniący wprowadza bzdury?
Podsumowując, jeśli testujesz każdą kombinację wartości, prawie na pewno robisz to źle (i prawdopodobnie skończy się kombinatoryczną eksplozją warunkowych). Jeśli chodzi o TDD, powinieneś napisać minimalną liczbę przypadków testowych niezbędnych do uzyskania pożądanego algorytmu. Wszelkie dalsze testy, które napiszesz, zaczną być zielone, a tym samym staną się dokumentacją, a nie ściśle częścią procesu TDD. Kolejne przypadki testowe TDD napiszesz tylko wtedy, gdy wymagania się zmienią lub błąd zostanie ujawniony, w którym to przypadku udokumentujesz niedobór testem, a następnie go przejdziesz.
Aktualizacja:
Zacząłem to jako komentarz w odpowiedzi na twoją aktualizację, ale zaczęło być dość długo ...
Powiedziałbym, że problem nie polega na istnieniu literałów, kropki, ale na tym, że „najprostsze” jest 5-częściowe warunkowe. Kiedy się nad tym zastanowić, 5-częściowy warunek jest w rzeczywistości dość skomplikowany. Powszechne będzie stosowanie literałów podczas kroku od czerwonego do zielonego, a następnie ich abstrakcja do stałych na etapie refaktoryzacji lub uogólnianie ich w późniejszym teście.
Podczas mojej własnej podróży z TDD zdałem sobie sprawę, że należy dokonać istotnego rozróżnienia - nie jest dobrze pomylić „proste” i „tępe”. To znaczy, kiedy zaczynałem, patrzyłem, jak ludzie robią TDD, i myślałem, że „robią najgłupsze możliwe testy, aby testy przeszły pomyślnie” i przez pewien czas naśladowałem to, dopóki nie zdałem sobie sprawy, że „proste” było nieco inne niż „tępy”. Czasami pokrywają się, ale często nie.
Przepraszam, gdybym sprawiał wrażenie, że problemem jest istnienie literałów - nie jest. Powiedziałbym, że problemem jest złożoność warunku z 5 klauzulami. Twój pierwszy kolor czerwony do zielonego może być po prostu „prawdziwy”, ponieważ jest to naprawdę proste (i tępe, przypadkowo). Następny przypadek testowy z (1, 2, 3, 4, 5) będzie musiał zwrócić false, i wtedy zaczynasz pozostawiać „rozwarty”. Musisz zadać sobie pytanie „dlaczego (1, 1, 1, 2, 2) to fula, a (1, 2, 3, 4, 5) nie jest?” Najprostszą rzeczą, jaką możesz wymyślić, może być to, że jeden ma ostatni element sekwencji 5 lub drugi element sekwencji 2, a drugi nie. Są proste, ale są też (niepotrzebnie) tępe. To, na czym tak naprawdę chcesz jechać, to „ile mają takich samych numerów?” Możesz więc zdać drugi test, sprawdzając, czy jest powtórzenie. W jednym z powtórzeniami masz fula, aw drugim nie. Teraz test kończy się pomyślnie, a ty piszesz kolejny przypadek testowy, który ma powtórzenie, ale nie jest pełny, aby dopracować algorytm.
Możesz robić to z literałami lub nie, a jeśli tak, to w porządku. Ale ogólną ideą jest rozwijanie algorytmu „organicznie” w miarę dodawania kolejnych przypadków.
źródło
Testowanie pięciu konkretnych wartości literalnych w określonej kombinacji nie jest „najprostsze” dla mojego gorączkowego mózgu. Jeśli rozwiązanie problemu jest naprawdę oczywiste (policz, czy masz dokładnie trzy i dokładnie dwie dowolne wartości), to napewno napisz kod tego rozwiązania i napisz kilka testów, których bardzo, bardzo mało prawdopodobne jest przypadkowe spełnienie ilość napisanego kodu (tj. różne literały i różne rzędy potrójnych i podwójnych).
Maksymy TDD naprawdę są narzędziami, a nie przekonaniami religijnymi. Chodzi o to, aby szybko napisać poprawny, dobrze przemyślany kod. Jeśli maksyma oczywiście stoi na przeszkodzie temu, po prostu przeskocz do przodu i przejdź do następnego kroku. W twoim projekcie będzie wiele nieoczywistych bitów, w których możesz go zastosować.
źródło
Odpowiedź Erika jest świetna, ale pomyślałem, że mogę podzielić się pewną sztuczką podczas pisania testu.
Zacznij od tego testu:
Ten test staje się jeszcze lepszy, jeśli utworzysz
Roll
klasę zamiast zaliczać 5 parametrów:To daje tę implementację:
Następnie napisz ten test:
Gdy to minie, napisz to:
Potem założę się, że nie musisz już pisać (może dwie pary, a może yahtzee, jeśli uważasz, że to nie jest fula).
Oczywiście, zastosuj dowolne metody, aby zwrócić losowe Rzuty spełniające twoje kryteria.
Podejście to ma kilka zalet:
źródło
IsFullHouse
rzeczywiście powrócićtrue
jeślipairNum == trioNum
?Mogę wymyślić dwa główne sposoby, które wziąłbym pod uwagę podczas testowania tego;
Dodaj „kilka” więcej przypadków testowych (~ 5) prawidłowych zestawów full-house i taką samą liczbę oczekiwanych fal ({1, 1, 2, 3, 3}) jest dobrym. Pamiętaj, że na przykład 5 może być rozpoznane jako „3 takie same plus para” przez niepoprawną implementację). Ta metoda zakłada, że programista nie tylko próbuje zdać testy, ale faktycznie zaimplementuje go poprawnie.
Przetestuj wszystkie możliwe zestawy kości (jest tylko 252 różnych). To oczywiście zakłada, że masz jakiś sposób, aby dowiedzieć się, jaka jest oczekiwana odpowiedź (podczas testowania jest to znane jako
oracle
.) Może to być referencyjna implementacja tej samej funkcji lub istoty ludzkiej. Jeśli chcesz być bardzo rygorystyczny, warto ręcznie kodować każdy oczekiwany wynik.Tak się składa, że napisałem kiedyś sztuczną inteligencję Yahtzee, która oczywiście musiała znać zasady. Kod części oceniającej wyniki można znaleźć tutaj , należy pamiętać, że implementacja dotyczy wersji skandynawskiej (Yatzy), a nasza implementacja zakłada, że kostki są podawane w posortowanej kolejności.
źródło
Ten przykład naprawdę nie ma sensu. Mówimy tutaj o jednej prostej funkcji, a nie o projekcie oprogramowania. Czy to trochę skomplikowane? tak, więc to rozbicie. I absolutnie nie testujesz wszystkich możliwych danych wejściowych z 1, 1, 1, 1, 1 do 6, 6, 6, 6, 6, 6. Ta funkcja nie wymaga porządku, a jedynie kombinację, a mianowicie AAABB.
Nie potrzebujesz 200 osobnych testów logicznych. Możesz na przykład użyć zestawu. Prawie każdy język programowania ma wbudowany:
A jeśli dostaniesz wkład, który nie jest prawidłowym rzutem Yahtzee, powinieneś rzucić, jakby nie było jutra.
źródło