Na czym polega TDD

Wprowadzenie do Test Driven Development - wszystkie wpisy

W poprzedniej części cyklu o TDD opisałem dlaczego sposób wytwarzania oprogramowania, który praktykowałem na początku się nie sprawdzał i co mnie skłoniło do zainteresowania się Test Driven Development. Dzisiaj opiszę jak wygląda praca zgodnie z TDD. Jak to często bywa w przypadku praktyk zwinnych zasady teoretyczne są dosyć proste, a kluczem do sukcesu jest dyscyplina.

Czym jest TDD

Na początku musimy sobie wyjaśnić jedną bardzo ważną kwestię. TDD to nie synonim do pisania testów jednostkowych. Owszem, unit testy są ważnym elementem tej techniki, ale samo ich pisanie nie jest równoznaczne z pracą według TDD.

Test Driven Development to technika wytwarzania oprogramowania polegająca na pisaniu unit testów jednocześnie z kodem produkcyjnym. Praca odbywa się w krótkich cyklach, w których najpierw piszemy unit test, a następnie kod produkcyjny, który go przechodzi. Dzięki temu otrzymujemy potwierdzenie, że kod działa zgodnie z założeniami. Dodatkowo zapewniamy lepszą modułowość rozwiązania i uzyskujemy możliwość bezpiecznego refactoringu.

Mikrocykl TDD

Pisząc zgodnie z TDD powtarzamy w kółko trzy kroki nazywane mikrocyklem TDD albo TDD Mantra. Można je streścić w następujący sposób:

  • Dopisz tylko tyle kodu testowego, żeby otrzymać failujący test.
  • Dopisz tylko tyle kodu produkcyjnego, żeby wszystkie istniejące testy zaczęły przechodzić.
  • Kiedy wszystkie testy przechodzą wykonaj refactor.

Opisane wyżej fazy noszą nazwę Red – Green – Refactor. Red oznacza czerwony pasek w narzędziu wizualizującym wyniki testów mówiący, że testy failują, a green zielony oznaczający, że wszystkie testy przechodzą.

Faza Red

Na początku pisania możesz mieć dużo pomysłów na testy, ale ogranicz się do implementacji tylko jednego, który nie przechodzi. Resztę możesz na razie trzymać w formie komentarza. Napisanie testu przed implementacją jest rodzajem sformułowania naszych intencji. Skłania to nas do zastanowienia, co chcemy osiągnąć oraz do analizy otrzymanych wymagań i sformułowania pomocniczych założeń. Warto tutaj zaznaczyć, że wszelkiego rodzaju błędy kompilacji oraz runtime również oznaczają fail. Zaczynając pisać nowy moduł piszemy pierwszy test nie posiadając jeszcze funkcji API. Dopiero failujący test zmusza nas do ich napisania.

Faza Green

W tej fazie nie interesuje Cię piękno kodu, masz tylko doprowadzić do przechodzenia testów. Jak już to osiągniesz, wyczyścisz kod w fazie Refactor wiedząc, że nie psujesz funkcjonalności. W tej fazie skupiamy się na jak najprostszym osiągnięciu celu, jakim jest przejście testu. Piszemy jak najprostszą implementację, która spełni to zadanie. Jeżeli nie ma testu sprawdzającego daną funkcjonalność, a wiemy że moduł musi ją realizować, nie piszemy jej! Czekamy, aż powstanie stosowny test. Dzięki temu mamy pewność, że działanie każdego fragmentu kodu produkcyjnego jest sprawdzane przez odpowiednie testy. Szczególnie w początkowej fazie powstawania danego modułu może to być nieintuicyjne. Na przykład piszemy funkcję zwracającą wartość podaną na sztywno mimo, że wiemy, iż docelowo będzie musiała coś obliczać.

Faza Refactor

Mając możliwość sprawdzenia, że nie zepsuliśmy funkcjonalności, wykonywany refactor może być dużo bardziej agresywny. Nie musimy ograniczać się jedynie do zmiany nazw zmiennych i dodawania komentarzy ze strachu, że większa zmiana struktury kodu spowoduje utratę istniejących funkcjonalności. To jest jedna z największych zalet TDD wpływająca na jakość powstającego kodu.

Kolejne iteracje cyklu

Po wykonaniu całego cyklu dochodzimy z powrotem do fazy Red i zastanawiamy się jakich założeń nasz kod produkcyjny jeszcze nie spełnia, albo jakich warunków brzegowych, czy złośliwych konfiguracji danych wejściowych nie obsługuje. Często podczas fazy Green przychodzą nam do głowy przypadki, które nie są uwzględnione w testach. Należy je wtedy spisać np. w formie komentarza i dodać w fazie Red.

Dzięki częstemu zmienianiu punktu widzenia związanemu z przechodzeniem do kolejnych faz cyklu umysł pracuje trochę inaczej niż kiedy rzuci się w wir kodowania. Dzięki temu łatwiej przychodzi znajdowanie kolejnych scenariuszy, które należy sprawdzić.

Kolejne obroty cyklu i powstawanie nowych testów stopniowo wymusza poprawną implementację. Początkowo aby test przechodził najłatwiej jest zwracać wartości na sztywno. Jednak w miarę jak dodajemy kolejne przypadki stopień skomplikowania logiki próbującej w ten sposób oszukać test staje się dużo większy niż przy napisaniu poprawnej implementacji. W ten sposób naturalnie promujemy najprostsze rozwiązanie problemu.

Pojedynczy cykl Red – Green – Refactor powinien trwać bardzo krótko. Zwykle od kilkudziesięciu sekund do kilku minut. Podczas tego cyklu bardzo często wykonujemy build i uruchamiamy test. Dzięki temu szybko dostajemy feedback dotyczący błędów i warningów oraz runtime errorów.

Build unit testu

Build unit testowy wykorzystuje pojedynczy moduł kodu produkcyjnego. Oznacza to, że testowany kod produkcyjny jest budowany w oderwaniu od reszty aplikacji, a w miejsce wszystkich zależności potrzebnych do działania modułu tworzone są podstawione funkcje testowe, czyli tak zwane mocki. Dzięki temu unit test sprawdza tylko, czy do zewnętrznych modułów przekazujemy dobre dane wejściowe i poprawnie interpretujemy zwracane przez nie wartości. To co się dzieje z danymi po przekazaniu do zewnętrznego modułu testujemy oczywiście oddzielnie w unit teście dla tego modułu.

Uzależnienie od zielonego paska

Pisanie w TDD wymaga kompletnego przestawienia się i zmiany nawyków. Po czasie może to doprowadzić do tzw. green bar addiction. Pisanie małych fragmentów kodu i szybkie dostawanie informacji zwrotnej, że ten kod działa jest bardzo motywujące i daje uczucie satysfakcji. Programując klasycznie mamy to uczucie dopiero kiedy skończymy całą funkcjonalność. Tutaj asystuje nam ono dużo częściej sprawiając, że pisanie według TDD jest przyjemniejsze. Oczywiście może mieć to i złe strony, kiedy zielony pasek przysłoni nam racjonalne myślenie i np. przesadzamy z ilością i szczegółowością testów.

Utrzymywanie modułu po napisaniu

Napisanie modułu od zera zgodnie z TDD jest tą przyjemniejszą częścią prac nad kodem. Jednak kiedy moduł już istnieje, niezbędne jest jego utrzymanie. W ramach utrzymania wykonujemy takie prace jak fixowanie bugów i dodawanie nowych funkcjonalności. W takim wypadku głównym problemem jest fakt, że musimy zajrzeć od istniejącego modułu i przypomnieć sobie kontekst. Napisane wcześniej testy są w tym bardzo pomocne, ponieważ pokazują na przykładach spodziewane zachowanie modułu w różnych przypadkach. Aby poprawić buga, albo doddać funkcjonalność, należy zastosować taki sam cykl Red – Green – Refactor jak podczas zwykłego pisania modułu. Oznacza to, że zaczynamy od dopisania nowego testu, który nie przechodzi, a następnie wykonujemy kolejne fazy. W przypadku zmiany funkcjonalności możliwe, że zamiast dopisywania nowego testu, będziemy musieli zedytować istniejący.

Przykład

Na moim GitHubie umieściłem przykład modułu tworzonego przy pomocy TDD, który omawiam w trakcie szkolenia. Kolejne commity obrazują poszczególne kroki tworzenia modułu. W opisach commitów zawarłem informacje opisujące co się dzieje w danym momencie. Przykład został napisany w języku C przy pomocy frameworka testowego Unity, który jest zgodny z xUnit.

Podsumowanie

Jak wspomniałem wcześniej Test Driven Development na papierze wygląda bardzo prosto. I jak się go nauczymy, a pisanie testu przed kodem produkcyjnym i praca w krótkich cyklach wejdą nam w krew, rzeczywiście będzie prosty. Przy okazji praca zgodnie z TDD będzie bardziej produktywna od pisania w sposób klasyczny, a wynikowy kod będzie miał mniej głupich błędów logicznych, które pojawiają się podczas pisania. Jednak aby to osiągnąć należy mieć nieco samozaparcia i przyzwyczaić się do pisania w ten sposób. Na początku ważne jest, aby nie ulegać pokusie pisania kodu produkcyjnego i otestowywania go później oraz nie pisać skomplikowanych testów wymagających wydłużenia czasu potrzebnego na pojedynczy cykl Red – Green – Refactor.

Wprowadzenie do Test Driven Development - Nawigacja

3 Comments

  1. „Test Driven Development na papierze wygląda bardzo prosto” —> ano, na papierze, bo w komercyjnych projektach z czasem pojawia się trochę problemów – to podejście bardzo mocno wpływa na architekturę projektu, kod staje się bardzo rozdrobniony, pojawia się mnóstwo małych funkcji (i testów do nich) – po pewnym czasie można „nie połapać się” w całości. Dodatkowo dochodzą kwestie mockowania zależności i… wiele innych problemów;) TDD wchodzi za to bardzo gładko przy pisaniu wszelakich funkcji „bibliotecznych”:)

  2. Będzie kontynuacja wpisów nt. TDD?

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *