TDD

Zasada jednej odpowiedzialności

Test Driven Development.

Współcześnie jest wiele zasad pisania poprawnego kodu i metod jego testowania, którymi kierują się obecni programiści. Test Driven Development czyli TDD to podejście do tworzenia i testowania oprogramowania. Twórcą takiego podejścia jest Kent Beck. Podejście tej metody zakłada, że przed napisaniem właściwej funkcjonalności programista zaczyna od utworzenia testu. Test ten powinien testować funkcjonalność, którą dopiero chcemy napisać. W tym artykule opisze tą metodę, przedstawię przykład kodu testu oraz opiszę do czego służy i czym się różni Stub, Spy oraz Mock.

Wstęp

Test Driven Development (TDD) to technika tworzenia oprogramowania, zaliczana do metodyk zwinnych. Pierwotnie była częścią programowania ekstremalnego, lecz obecnie stanowi samodzielną technikę. Polega ona na wielokrotnym powtarzaniu 3 kroków:

Test Driven Development

Zdjęcie 1. Metoda TDD

Faza Red

Pierwszym krokiem jest napisanie testu. Test ten nie może się powieść, ponieważ sama funkcjonalność jeszcze nie jest zaimplementowana. Możliwe, że nawet po napisaniu takiego testu kod nie będzie się kompilował. Może się tak stać w przypadku, gdy napisałeś test dla metody, która jeszcze nie istnieje. Sytuacja, w której testy jednostkowe nie przechodzą bardzo często w IDE oznaczana jest kolorem czerwonym.

Faza Green

Kolejnym krokiem jest napisanie kodu, który implementuje brakującą funkcjonalność. W tym momencie istotne jest to aby ten kod nie był „idealny”. Chodzi o możliwe jak najszybszą implementację, która spełnia założenia testu, który był napisany w poprzedniej fazie. Następnie potwierdzamy to, że nasza implementacja działa jak powinna uruchamiając testy jednostkowe. Jeśli wszystko jest w porządku całość powinna zakończyć się testami jednostkowymi, które przechodzą. IDE sygnalizuje taką sytuację zielonym kolorem. Ważne jest aby w tej fazie uruchamiać wszystkie dotychczas napisane testy jednostkowe.

Faza Refactor

Refaktoryzacja (ang. refactor) to proces, w którym zmieniamy kod w taki sposób, że nie zostaje zmieniona jego funkcjonalność. Mówi się o „oczyszczaniu” kodu, doprowadzaniu go do lepszego stanu. Przykładem refaktoryzacji może być wydzielenie oddzielnej metody, która usuwa powielony kod czy stworzenie zupełnie nowej klasy odpowiedzialnej za pewną część zadań danej klasy.

Jest to ostatnia z trzech faz cyklu TDD. Faza refaktoryzacji jest bardzo istotna. Nawet doświadczeni programiści bardzo często pomijają tę fazę. Jej brak może w dłuższej perspektywie prowadzić do kodu programu, który jest trudny w utrzymaniu. Praca z takim kodem może być wówczas dużo cięższa, proste zmiany mogą zajmować bardzo dużo czasu.

Dzięki testom, które napisałeś w fazie Red czy wcześniejszych cyklach TDD, możesz czuć się swobodnie zmieniając istniejący kod. Z większą pewnością możesz zmieniać kod, po każdej zmianie uruchamiając istniejące testy jednostkowe. Takie podejście pozwala Ci bardzo szybko wychwycić potencjalne błędy, które mógłbyś wprowadzić refaktoryzując kod.

Może się zdarzyć, że faza refaktoryzacji nie zawsze jest konieczna. Usprawnianie dobrego kodu na siłę nie koniecznie może prowadzić do dobrych rezultatów.

Przykład

Poniżej utworzona jest cześć kodu testującego według TDD:

  1. public class MealRepositoryTest  
  2. {  
  3.     @Test  
  4.     void shouldBeAbleToAddMealToRepository()  
  5.     {  
  6.        //given  
  7.        MealRepository mealRepository = new MealRepository();  
  8.     }  
  9. }

Oczywiście ten test nie przechodzi, gdyż brakuje nam klasy MealRepository, zatem wykonaliśmy pierwszy krok z metody TDD – krok Red. Następnie tworzymy klasę MealRepository jak poniżej:

  1. public class MealRepository {}

Test nam przechodzi czyli wykonaliśmy kolejny krok – krok Green. Następnym krokiem jest krok Refactor, jednakże w naszym kodzie jak na razie nie musimy nic poprawiać w kodzie, wiec jeden cykl testu TDD wykonaliśmy poprawnie. Wykonujemy teraz drugi cykl testu TDD, dodając nowy kod:

  1. public class MealRepositoryTest  
  2. {  
  3.     @Test  
  4.     void shouldBeAbleToAddMealToRepository()  
  5.     {  
  6.         //given  
  7.         MealRepository mealRepository = new MealRepository();  
  8.         Meal meal = new Meal(10, "Pizza");  
  9.         //when  
  10.         mealRepository.add(meal);  
  11.     }  
  12. }

Jednakże, test nam nie przechodzi bo nie mamy dodanej metody add do klasy MealRepository (krok pierwszy został wykonany), przechodzimy do kroku dwa, dodajemy metodę add do klasy:

  1. public class MealRepository
  2. {
  3.     public void add(Meal meal) {}  
  4. }

Po dodaniu metody test nam przechodzi (krok drugi z metody TDD został zrealizowany). Krok trzeci nie musimy jak na razie wykonywać, bo rekatoryzacja kodu jest jeszcze zbędna. Kolejny cykl metody TDD został wykonany. Jak widać postępujemy bardzo iteracyjnie, dodajemy tyle kodu ile jest tylko potrzeba, aby test przeszedł na zielono, nie więcej. Następne cykle wykonujemy analogicznie do powyższego przykładu, dodając jak najmniej kodu, sprawdzając najpierw czy test nie przechodzi, kolejno tworząc kod, żeby test nam przeszedł i na koniec próbujemy poprawić jakoś kodu.

Zalety stosowania TDD

Najwyżej na liście korzyści dla praktykujących TDD są:

  • Szybkie rezultaty: programiści mogą zobaczyć efekt decyzji projektowych w ciągu kilku minut.
  • Elastyczność: zmiany są łatwe ze względu na niewielką odległość między zatwierdzeniami.
  • Automatyczny katalog testów regresji: jeśli coś opracowane sześć miesięcy temu nagle złamie się pod dzisiejszym kodem, jest znane natychmiast.
  • Dobry, czysty i działający kod.

Istotną zaletą TDD jest to, że umożliwia on wykonywanie małych kroków podczas pisania oprogramowania. Jest to popularna praktyka, ponieważ jest o wiele bardziej produktywna niż próba kodowania dużymi krokami. Wyniki dla dziesięciu różnych wewnętrznych i zewnętrznych atrybutów jakości wskazują, że rozwój sterowany testami może zmniejszyć liczbę wprowadzanych defektów i prowadzić do łatwiejszego utrzymania kodu.

Stub

Stuby to przykładowe implementacje jakiegoś kodu, którego zachowanie chcemy przetestować. Są nierozłącznie związane z testami jednostkowymi. W Stubie podajemy przykładową implementację interfejsu w której podajemy przykładowe dane. Dla prostych metod Stuby działają bardzo dobrze ale dla rozbudowanych interfejsów są kiepskim rozwiązaniem. Stub potrafi zwracać zdefiniowane przez nas wartości, o ile o nie poprosimy. Stub też nie wyrzuci błędu, jeśli nie zdefiniowaliśmy danego stanu (np. metody void są puste, a niezdefiniowane wartości wyjścia zwracają wartości domyślne).

Przykładowy test:

  1. [Test]
  2. public void StubExample()
  3. {
  4.     var customerValidator = new CustomerValidator();
  5.     var customer = Mock.Of<ICustomer>(c => c.GetAge() == 21);
  6.     // Stub  
  7.     bool validate = customerValidator.Validate(customer);
  8.     validate.Should().BeTrue();
  9. }

Kod 1. Kod testu Stub

Mock

Mock (z ang. imitacja, atrapa) potrafi weryfikować zachowanie obiektu testowanego. Jego celem jest sprawdzenie czy dana składowa została wykonana. Mocki to obiekty symulujące prawdziwe zachowania obiektów i prawdziwego kodu. Mogą być tworzone dynamicznie podczas działania programu i są bardziej elastyczne niż stuby. Mają większą funkcjonalność.

Przykładowy test:

  1. [Test]
  2. public void MockExample()
  3. {
  4.     var customerValidator = new CustomerValidator();  
  5.     var customer = Mock.Of<ICustomer>(c => c.GetAge() == 21);  
  6.     // Mock  
  7.     customerValidator.Validate(customer);
  8.     Mock.Get(customer).Verify(x => x.GetAge()); //Verification of Mock
  9. }

Kod 2. Kod testu Mock

Spy

Spy (z ang. szpieg) to mock z dodatkową funkcjonalnością. O ile mock rejestrował czy dana składowa została wywołana, to spy sprawdza dodatkowo ilość wywołań. Spy jest wrapper, obiektem opakowującym obiekt danej klasy , którego działanie możemy śledzić oraz weryfikować podobnie jak obiekty mockowe i możemy również mockować działanie wybranych metod. Obiekt spy są częściowo mockiem a częściowo normalnym obiektem.

Przykładowy test:

  1. [Test]
  2. public void SpyExample()
  3. {
  4.     var customerValidator = new CustomerValidator();
  5.     var customer = Mock.Of<ICustomer>(c => c.GetAge() == 21);
  6.     // Spy  
  7.     customerValidator.Validate(customer);
  8.     Mock.Get(customer).Verify(x => x.GetAge(), Times.Once); // Verification of Spy  
  9. }

Kod 3. Kod testu Spy

Podsumowanie

Podsumowując, testowanie kodu programu jest bardzo istotnym elementem w tworzeniu oprogramowania, pomagającym nam w łatwiejszy i szybszy sposób znaleźć błędy w programie. TDD jest jedną z takich metod testowych, gdzie pierwsze tworzymy test a potem piszemy prawidłową funkcjonalność. Jednakże, Test Driven Development nie należy traktować jako „wyrocznię”, jedyną słuszną technikę tworzenia oprogramowania. Ma ona swoje zalety i wady. Musimy pamiętać o tym, żeby dobierać narzędzia oraz techniki do indywidualnych potrzeb projektu.