TDD dla początkujących. Cz. 2: Prosty przykład

Jako pierwszy przykład zastosowania TDD, postanowiłem zaimplementować aplikację zwracającą wartość ciągu Fibonacciego, gdzie daną wejściową jest pozycja szukanej w ciągu. Pomysł nie jest mój- został zaczerpnięty z książki, autorstwa Kent’a Beck’a, pod tytułem „TDD Sztuka tworzenia dobrego kodu”, wydawnictwa Helion.

Mimo, że zadanie przed którym stoimy jest trywialne, to jednak wydaje mi się bardzo wartościowe dla programisty, który nie miał wcześniej nic wspólnego z TDD. Dlaczego? O tym w trakcie :).

Na początku, pro forma, powiedzmy sobie czym jest ciąg Fibonacciego. Zacznijmy od końca :). Leonardo Fibonacci był włoskim matematykiem, żyjącym na przełomie XII i XIII wieku. Jest autorem książki Liber abaci („Księga liczydła”),  która była min. zbiorem problemów matematycznych i ich rozwiązań. To właśnie tam – jako rozwiązanie jednego z problemów – autor wykorzystał ów ciąg, który dzisiaj chcemy zaimplementować krocząc ścieżką TDD :). Co ciekawe sam ciąg Fibonacciego stał się, pewnego rodzaju fenomenem – ponoć Antonio Stradivari, korzystał z niego dla wyznaczenia proporcji części w swoich skrzypcach, literaci często wykorzystują motyw tego ciągu w swoich utworach, a programiści, często ucząc innych, wykorzystują go do przedstawienia koncepcji rekurencji :).

P.S:  Matematycy toczą odwieczny spór dotyczący postrzegania liczby 0 jako naturalnej. Ma to dla nas znaczenie, bo ciąg Fibonacciego to ciąg liczb naturalnych, i w zależności od tego jak postrzegamy zero, tak będzie wyglądał początek ciągu. Nawet jeżeli się z tym nie zgadzacie, przyjmijmy że 0 jest liczbą naturalną :).


 

Przykład:
Na początek wymagania:

Musimy stworzyć aplikację konsolową, gdzie użytkownik może podać liczbę oznaczającą pozycję elementu ciągu Fibonacciego, a program zwraca wartość tego elementu. Wiemy, że:
F[0] = 0
F[1] = 1
F[n] = F[n-2] + F[n-1] , dla n > 1

Z tą wiedzą możemy zaczynać 🙂 :

1 – Utwórzmy dwa projekty: jeden to standardowa aplikacja konsolowa, a drugi biblioteka(dla testów jednostkowych). W przykładzie korzystam z frameworka xUnit. 

projects

2- Teraz możemy zacząć pisanie. Zaczynamy oczywiście od testów. Musimy utworzyć nową klasę w projekcie FibonacciTDD.Tests, która będzie zawierała testy danej funkcjonalności (klasy z projektu FibonacciTDD, która jeszcze nie istnieje :)). Konwencja nazw, którą stosuje dla klas testów wygląda tak : NazwaTestowanejKlasyTests, w związku z tym, że jeszcze nie wiemy jak będzie się nazywała aktualnie testowana klasa, tymczasowo możemy ją nazwać FirstClassTests. Jednocześnie możemy napisać pierwszy test, który nazwiemy, a jakże, FirstTest :

FibonacciStep1

3- Teraz wypadałoby napisać ten FirstTest. Zastanówmy się co wiemy, i jakie możemy zadać pytania. F[0] = 0, więc możemy sprawdzić czy faktycznie tak jest. Musimy się też zastanowić nad sposobem uzyskania tego wyniku. Za obliczanie będzie odpowiedzialna klasa FibonacciNumberGetter, wyposażona w metodę GetResult(). Mając to w myślach (i na razie tylko tam!) napiszmy test (jednocześnie ustawimy środowisko testowe):

FibonacciStep2

4- Chcąc uruchomić nasz test, musimy zbudować solucję. Oczywiście jest to niemożliwe, ponieważ nie istnieje ani klasa FibonacciNumberGetter, ani tym samym metoda GetResult. Naszym celem w tym kroku jest doprowadzenie to zbudowania solucji. Tworzymy więc wspomnianą klasę, w projekcie FibonacciTDD, łącznie z metodą GetResult, i nic ponadto. Metoda niech rzuca wyjątek NotImplementedException.

5- Uruchamiamy testy. Jest postęp bo solucja się buduje, natomiast testy się załamują. Osiągnęliśmy pierwszy punkt wzorca Red-Green-Refactor. Teraz, jak najszybciej i za wszelką cenę, musimy dojść do punktu drugiego – green, czyli sprawić by testy przechodziły. Nasz test w tej chwili oczekuje, że GetResult zwróci 0. Zaspokoimy więc te oczekiwania, edytując ciało naszej metody w taki sposób:FibonacciStep3

6- Może się to wydawać dziwne, na pierwszy rzut oka, ale w tej chwili, czyli w momencie gdy testy nie przechodzą najważniejsze jest zmienienie tego stanu rzeczy. Oczywiście możemy pisać pełniejszą implementację, tylko od nas zależy rozmiar pojedynczego kroku (gdzie krok to akcje wykonane przez nas między kolejnym uruchomieniem testów – o tym zresztą będziemy jeszcze mówić w przyszłości) , natomiast uważam, że na potrzeby przykładu warto jest przedstawić wszystko możliwie jak najbardziej „atomowo”.

Wracając do tematu: nasz test w tej chwili już przejdzie. kolejnym krokiem w naszym wzorcu postępowania(R-G-R) jest Refactor. Jest to moment, w którym spłacamy zaciągnięty dług, wykorzystany do zapewnienia przejścia testu, w poprzednim punkcie. W naszym przypadku jest to return 0; w metodzie GetResult. Nie jest to jednak takie proste. Feedback jaki otrzymaliśmy poprzez nasz test jest niewystarczający do wyciągnięcia wniosków na temat poprawnego kształtu testowanej metody. W takim wypadku, powinniśmy dodać kolejną asercję do naszego testu. Co możemy sprawdzić? Oczywiście: czy GetResult zwróci 1, jeżeli podamy argument 1. Oczywiście nasz test załamuje się. Znowu naszym priorytetem jest jak najszybsze osiągnięcie akceptacji testów. I znowu robimy to w niezbyt finezyjny sposób:

FibonacciStep4

7- Teraz testy przechodzą, ale próbując zabrać się za refactoring, znowu okazuje się, że ilość przypadków testowych jest niewystarczająca do zapewnienia nam potrzebnej wiedzy. Postanawiam, aby się nie rozdrabniać, trochę powiększyć swój krok i dodać od razu dwa nowe przypadki testowe, dla pozycji  7 i 14 (losowo wybranych, byleby większych od 1, bo wartości 0 i 1 są niejako „specjalne”). Postanawiam też w końcu zmienić nazewnictwo w naszym testowy projekcie. W końcu teraz wiemy już co testujemy :). Po naszych zmianach klasa testowa wygląda tak:

FibonacciStep5

8- I znowu cykl zaczyna się od początku. Testy nie przechodzą. Oczywiście  należy pamiętać, że nie jest to nic złego – oznacza to postęp i jednocześnie stawia przed nami cel. Tutaj znów postaramy się wydłużyć swój krok, nie możemy przecież zapełniać procedury coraz to nowymi „ifami” w celu akceptacji testów . Wiemy już, że nie tędy droga, choć nie do końca – wiemy, że warunek sprawdził się dla przypadku specjalnego 0, wiemy też że mamy jeszcze jeden przypadek specjalny 1. Wysnuwamy więc wniosek, że dla drugiego przypadku specjalnego, również skorzystamy z warunku. I na  tym koniec „ifów”. Wiemy też ,że F[n] = F[n-2] + F[n-1]. W takim wypadku możemy od razu spróbować napisać coś takiego:

FibonacciStep6

9- Teraz nasz test przechodzi. Możemy założyć, że wszystkie przypadki – jeżeli podamy poprawne, czyli większe od 0, wejście – są pokryte. Możemy spokojnie przejść do punktu 3 naszego wzorca postępowania, czyli do Refactor. W tak trywialnym przykładzie nie mamy wiele do roboty. Refaktoryzacja, na pierwszym miejscu, powinna się skupić na usuwaniu duplikacji. Jeżeli chodzi o sam kod „Produkcyjny” ;), to wydaje mi się, że może zostać w takiej postaci, gorzej z klasą zawierającą testy. Jak widzimy metoda GetResultTests, cztery razy sprawdza asercję, na różne dane testowe. Może nie jest to tragedia, ale jednak wypada to poprawić, bo co  w przypadku gdybyśmy chcieli zamiast 4 asercji, mieć 8 ? :). Zredukujmy więc tą duplikację, trzymając nasze przypadki testowe w słowniku, gdzie kluczem jest pozycja, a wartością… wartość 🙂 :

FibonacciStep7


 

Teoretycznie moglibyśmy zabezpieczyć się jeszcze przed wypadkiem, gdy na wejściu zostaje podana nieprawidłowa (tzn. mniejsza od 0) liczba oznaczająca pozycję. Myślę jednak, że to już niech będzie „zadaniem domowym” ;).

To by było na tyle, jeżeli chodzi o dzisiejszy wpis. W następnej części powiemy sobie kilka słów o zasadach i dobrych praktykach pisania testów jednostkowych przed rozpoczęciem pisania kodu. Gdzieś na horyzoncie majaczy też widmo trochę bardziej skomplikowanego przykładu… Ale na to jeszcze przyjdzie czas :).

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *