TDD dla początkujących – cz.2

Posted by Przemysław Owsianik on 2021-10-03
<p>Jako pierwszy przykład zastosowania TDD, postanowiłem zaimplementować&nbsp;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 <b>Kent’a Beck’a</b>, pod tytułem&nbsp;<b>„TDD Sztuka tworzenia dobrego kodu”</b>, wydawnictwa Helion.</p> <p>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 :).</p> <p>Na początku, pro forma, powiedzmy sobie czym jest ciąg Fibonacciego. Zacznijmy od końca :).&nbsp;<a href="https://pl.wikipedia.org/wiki/Fibonacci" target="_blank">Leonardo Fibonacci</a>&nbsp;był włoskim matematykiem, żyjącym na przełomie XII i XIII wieku. Jest autorem książki&nbsp;Liber abaci&nbsp;(„Księga liczydła”), &nbsp;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> <p>P.S: &nbsp;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ą :).</p> --- <h2>Na początek wymagania:</h2> <p>Musimy stworzyć aplikację konsolową, gdzie użytkownik może podać liczbę oznaczającą pozycję elementu ciągu Fibonacciego, a program zwraca wartość tego elementu.&nbsp;Wiemy, że:F[0]&nbsp;= 0F[1]&nbsp;= 1F[n]&nbsp;=&nbsp;F[n-2]&nbsp;+&nbsp;F[n-1]&nbsp;, dla n &gt; 1</p> <h2>Z tą wiedzą możemy zaczynać&nbsp;</h2> <p><b>1 </b>– Utwórzmy dwa projekty: jeden to standardowa aplikacja konsolowa, a drugi biblioteka(dla testów jednostkowych). W przykładzie korzystam z frameworka <b>xUnit</b>.&nbsp;</p> <img src="/static/img/blog/fibbProjects.png" alt="" class=" postImage" /> <p><b>2&nbsp;</b>– Teraz możemy zacząć pisanie. Zaczynamy oczywiście od testów. Musimy utworzyć nową klasę w projekcie <i>FibonacciTDD.Tests</i>, która będzie zawierała testy danej funkcjonalności (klasy z projektu <i>FibonacciTDD</i>, która jeszcze nie istnieje :)). Konwencja nazw, którą stosuje dla klas testów wygląda tak : <i>NazwaTestowanejKlasyTests</i>, w związku z tym, że jeszcze nie wiemy jak będzie się nazywała aktualnie testowana klasa, tymczasowo możemy ją nazwać <i>FirstClassTests</i>. Jednocześnie możemy napisać pierwszy test, który nazwiemy, a jakże, <i>FirstTest </i>:</p> <img src="/static/img/blog/FibonacciStep1.png" alt="" class=" postImage" /> <p><b>3</b>&nbsp;– Teraz wypadałoby napisać ten <i>FirstTest</i>. 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 <i>FibonacciNumberGetter</i>, wyposażona w metodę <i>GetResult()</i>. Mając to w myślach (i na razie tylko tam!) napiszmy test (jednocześnie ustawimy środowisko testowe):</p> <img src="/static/img/blog/FibonacciStep2.png" alt="" class=" postImage" /> <p><b>4</b> – Chcąc uruchomić nasz test, musimy zbudować solucję. Oczywiście jest to niemożliwe, ponieważ nie istnieje ani klasa <i>FibonacciNumberGetter</i>, ani tym samym metoda <i>GetResult</i>. Naszym celem w tym kroku jest doprowadzenie to zbudowania solucji. Tworzymy więc wspomnianą klasę, w projekcie <i>FibonacciTDD</i>, łącznie z metodą <i>GetResult</i>, i nic ponadto. Metoda niech rzuca wyjątek <i>NotImplementedException</i>.</p> <p><b>5</b>&nbsp;– Uruchamiamy testy. Jest postęp bo solucja się buduje, natomiast testy się załamują. Osiągnęliśmy pierwszy punkt wzorca <i>Red-Green-Refactor</i>. 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 <i>GetResult </i>zwróci 0. Zaspokoimy więc te oczekiwania, edytując ciało naszej metody w taki sposób:</p> <img src="/static/img/blog/FibonacciStep3.png" alt="" class=" postImage" /> <p><b>6</b>&nbsp;– Może się to wydawać dziwne, na pierwszy rzut oka, ale w tej chwili,&nbsp;czyli w momencie gdy testy nie przechodzą&nbsp;najważniejsze jest zmienienie tego stanu rzeczy. Oczywiście możemy pisać pełniejszą implementację, tylko od nas zależy rozmiar pojedynczego&nbsp;kroku&nbsp;(gdzie&nbsp;krok to akcje wykonane przez nas między kolejnym uruchomieniem testów&nbsp;– o&nbsp;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”.</p> <p>Wracając do tematu: nasz test w tej chwili już przejdzie. kolejnym krokiem w naszym wzorcu postępowania(R-G-R) jest&nbsp;Refactor. Jest to moment, w którym&nbsp;spłacamy zaciągnięty dług, wykorzystany do zapewnienia przejścia testu, w poprzednim punkcie. W naszym przypadku jest to&nbsp;return 0;&nbsp;w metodzie&nbsp;<i>GetResult</i>.&nbsp;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 <i>GetResult </i>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:</p> <img src="/static/img/blog/FibonacciStep4.png" alt="" class=" postImage" /> <p><b>7</b>&nbsp;– 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:</p> <img src="/static/img/blog/FibonacciStep5.png" alt="" class=" postImage" /> <p><b>8</b> – 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:</p> <img src="/static/img/blog/FibonacciStep6.png" alt="" class=" postImage" /> <p><b>9</b>&nbsp;– 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 <i>GetResultTests</i>, 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ść 🙂 :</p> <img src="/static/img/blog/FibonacciStep7.png" alt="" class=" postImage" /> --- <p>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” ;).</p> <p>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 :).</p>