Spis Treści
Czym jest TDD
TDD czyli Test-Driven Development jest techniką pisania oprogramowania przewracającą podejście do tworzenia kodu o 180 stopni. Myślę, że intuicyjnym dla nas jest, że aby coś móc przetestować to należy najpierw to coś stworzyć, a dopiero potem przetestować. Podejście TDD zakłada coś zupełnie odwrotnego. Tworzenie funkcjonalności rozpoczynamy od napisania testu, a dopiero potem zaimplementowania kodu właściwego, który będzie testowany.
Istnieje przyjęty schemat, który bardzo dobrze pomaga zrozumieć jak należy do TDD podchodzić. Schemat ten jest tak naprawdę cyklem składającym się z trzech faz, który powtarzany jest iteracyjnie, aż do spełnienia przez kod produkcyjny wymaganej logiki biznesowej. Oto one:
- faza red,
- faza green,
- refactor

Faza RED – tworzenie testu
Jest to faza od której rozpoczynamy cykl TDD. Jego celem jest napisanie testu, który nie będzie przechodził podczas testowania i zawsze taki na początku powinien być. Piszemy jedną małą funkcjonalność testu i wówczas przechodzimy do kolejnej fazy.
Faza green – tworzenie kodu właściwego
W fazie tej należy zaimplementować MINIMALNĄ ilość kodu produkcyjnego, który pozwoli na przejście testu. Dopiero gdy test zaświeci się na zielono, przechodzimy do kolejnej fazy.
Faza Refaktoryzacji kodu
Faza ta polega na tym aby powstały kod, zarówno testowy jak i produkcyjny doprowadzić do jak najbardziej „eleganckiej” postaci. Ważnym jest aby nie traktować tej fazy po macoszemu ponieważ ona właśnie w dużej mierze pozwala na pisanie aplikacji czytelnej dla innych programistów, a także dobrze zoptymalizowanej.
Po zakończeniu cyklu wracamy do fazy red czyli pisania kolejnej małej funkcjonalności testu, sprawiając że test znów zaświeci się na czerwono. Następnie przechodzimy do kolejnych faz, aż do momentu, w którym logika biznesowa zostanie poprawnie zaimplementowana i testy będą przechodziły na zielono.
Po co w ogóle używać TDD
Skoro już wiemy jaka jest procedura postępowania przy używaniu metody TDD należy zatem zastanowić się po co w ogóle stosować to podejście. Jakiekolwiek pisanie testów sprawia, że trzeba napisać o wiele więcej kodu i nie ma to znaczenia czy piszemy testy po implementacji funkcjonalności czy też przed nią. Nie mniej jednak poświęcony czas zwraca się później z nawiązką.
Metoda TDD pozwala na stworzenie kodu, który będzie łatwo testowalny, ponieważ od testu wychodzimy i podążamy do jego przejścia. Początkowe wyznaczenie celu, który ma kod produkcyjny spełniać sprawia, że łatwiej napisać kod, który będzie się dobrze zachowywał w różnych warunkach brzegowych takich jak np. przekazywanie do funkcji i metod wartości null.
Napisanie wielu testów pozwala na późniejsze łatwe wychwytywanie różnych bugów, które mogą być trudne do namierzenia. Może się zdarzyć że będziesz musiał dokonać zmiany w już istniejącym kodzie. Jeśli napiszesz sporo testów, może okazać się że zaoszczędzisz wiele godzin na późniejszym poszukiwaniu błędu.
Jednakże najważniejszą wg mnie zaletą stosowania TDD jest fakt, że programista zmuszony jest do pisania testów, które niejednokrotnie po napisaniu działającego kodu schodzą na dalszy plan. TDD wymusza pewien rygor postępowania i porządkuje pracę. Dodatkowo każde przejście testu na zielono jest swego rodzaju nagrodą i z pewnością w dłuższej perspektywie motywuje do dalszej pracy.
Kilka zasad jak poprawnie pisać w TDD
Aby cała nasza praca poświęcona na pisanie testów nie poszła na marne warto jest stosować kilka podstawowych zasad:
Pisz testy jednostkowe
Testy jednostkowe mają to do siebie że testują mały fragment kodu, a co za tym idzie świetnie wpisują się w ideę aby poszczególne wdrażane funkcjonalności dzielić na mniejsze kroki i iteracyjnie dokładać kolejne cegiełki do kodu
Stosój zasadę given-when-then
Warto jest aby stosować te zasadę ponieważ bardzo dobrze organizuje ona kod i pozwala na łatwe i szybkie zrozumienie funkcjonalnosci który dany test sprawdza.
@Test
void testSummingTwoNumbers() {
//given
Calculator calculator = spy(Calculator.class);
given(calculator .getFirstValue()).willReturn(2);
given(calculator .getSecondValue()).willReturn(3);
//when
int result = calculator.sumNumbers();
//then
then(calculator ).should().getFirstValue();
then(calculator ).should().getSecondValue();
assertThat(result, equalTo(5));
}
powyższy kod omówię dokładniej w podrozdziale o obiekcie typu spy, lecz wyraźnie widać w nim 3 sekcje organizujące test.
testy Nazywaj intuicyjnie
Ważnym jest, aby nazwa testu jednoznacznie określała co dany test sprawdza. Nie bój się stosować dłuższych nazw jeśli jest to konieczne. Gdy jakiś test później zaświeci się na czerwono łatwo będzie Ci zanaleźć przyczynę tego że test nie przeszedł.
Testuj po refaktoryzacji
Należy pamiętać, że testy należy odpalać również po zrefaktoryzowaniu kodu. Czasem pozornie niewielka zmiana może przez nieuwagę doprowadzić do błędów, które zmienią działanie kodu.
Miej umiar
Szczególnie w początkowej fascynacji metodą TDD można nieco przesadzić z pokryciem kodu testami. Nie testujmy rzeczy oczywistych takich jak np. settery i gettery. Pamiętajmy że wykonanie testu zajmuje zazwyczaj niewiele czasu, lecz jeśli napiszemy tych testów kilkaset lub kilka tysięcy, odpalanie ich może być naprawdę czasochłonne.
Oddzielaj kod testowy od kodu produkcyjnego
Pamiętaj aby kod testu znajdował się w specjalnym przeznaczonym do tego miejscu. Zazwyczaj IDE i frameworki, w których pracujemy same zadbają o to aby kod produkcyjny był w katalogu src, a kod testowy w osobnym katalogu test.
Izoluj testy od środowiska pracy
Ważnym jest aby testy wykonywały się w izolacji od środowiska w którym pracujemy. Chodzi o to aby nie odwoływać się do plików lokalnych, baz danych. Z pomocą tutaj przychodzą obiekty typu mock, stub czy spy, o których przeczytasz w kolejnych akapitach.
Mock, Stub, Spy
Tytułowe pojęcia tyczą się obiektów, które tworzymy w celu przetestowania działania metod w teście. Można je nazwać swego rodzaju królikami doświadczalnymi. Czym dokładnie są te obiekty.
Stub
Jest to najbardziej prymitywny sposób aby utworzyć obiekt na którym będziemy wykonywać testy. Obiekt ten powstaje na podstawie stubowej klasy, która ma na celu zasymulowanie działania prawdziwej klasy. Używa się go np. wtedy gdy nie ma mamy dostępu do rzeczywistych klas, lecz znamy jej zasadę funkcjonowania, lub wtedy gdy testowane przez nas metody wymagają działania na danych, a jak wspominałem wcześniej należy izolować testy od środowiska pracy kodu.
Wadą stosowania takiego rozwiącania jest to, że w przypadku rozbudowania klasy, na której prowadzimy testy musimy również rozbudowywać w miarę potrzeb klasę stubową.
mock
Mock jest typem obiektu, który generowany jest automatycznie na podstawie klasy, którą chcemy przetestować. Zazwyczaj do używania mocków wymagane są dodatkowe biblioteki jak np. Mockito dla języka Java. Sposób tworzenia obiektu jest bardzo prosty, a w przypadku Mockito wyglądać może np. tak:
MySampleClass mySampleClass= mock(mySampleClass.class);
Ta jedna linia kodu sprawia, że tworzony jest nowy obiekt, którego pola zostają wypełnione prostymi danymi, jak np. dla pól integer przypisywane są wartości 0. Zaletą tego rozwiązania jest to, że nie musimy tworzyć oddzielnych klas na potrzeby testów Każda zmiana w klasie którą mockujemy jest automatycznie uwzględniana przy tworzeniu mocka. Dodatkowo biblioteki takie jak Mockito dają nam wiele narzędzi do sprawdzania jakie parametry zostały użyte przy wywołaniu danej metody czy też zlicza np. krotność wywołania danej metody.
Spy
Jest to swego rodzaju połączenie mocka i stuba. Jego główną zaletą jest to że daje programiście elastyczność ponieważ jest tworzony na podstawie klasy, której działanie symulujemy, natomiast możemy narzucić np. jakie wartości mają poszczególne metody zwrócić i użyć ich potem do przetestowania innej metody.
@Test
void testSummingTwoNumbers() {
//given
Calculator calculator = spy(Calculator.class);
given(calculator .getFirstValue()).willReturn(2);
given(calculator .getSecondValue()).willReturn(3);
//when
int result = calculator.sumNumbers();
//then
then(calculator ).should().getFirstValue();
then(calculator ).should().getSecondValue();
assertThat(result, equalTo(5));
}
W powyższym kodzie zasymulowaliśmy zwracanie wartości z getterów, a następnie otrzymane wartości użyliśmy do przetestowania kodu sumującego dwie liczby w metodzie sumNumbers():
int sumNumbers(){
return getFirstValue() + getSecondValue();
}
Każdy z pokazanych sposobów ma swoje wady i zalety, ale dają szeroki wachlarz możliwości i są realnym usprawnieniem przy pisaniu testów aplikacji.
Pingback: WetApp - Szybka Prognoza Pogody – Wojciech Siwek
Możliwość komentowania została wyłączona.