Test Driven Development
Czym jest Test Driven Development?
Test Driven Development (TDD) to podejście do tworzenia oprogramowania, w którym proces pisania kodu jest prowadzony przez wcześniej przygotowane testy. Kluczową ideą TDD jest najpierw opisanie oczekiwanego zachowania aplikacji poprzez testy, a następnie napisanie kodu, który spełnia te testy. Dopiero po ich zaliczeniu można przystąpić do optymalizacji i ulepszania rozwiązania. Proces TDD jest iteracyjny i opiera się na cyklu zwanym Red-Green-Refactor.
Jak działa TDD?
- Red (Napisz test, który nie działa)
Najpierw programista pisze test, który opisuje oczekiwane zachowanie funkcjonalności. Test ten na początku nie przechodzi, ponieważ funkcjonalność jeszcze nie istnieje lub jest niepełna. - Green (Napisz minimalny kod, aby test przeszedł)
Programista implementuje minimalną ilość kodu potrzebną, aby test zakończył się sukcesem. Na tym etapie ważne jest, aby skupić się tylko na tym, co jest konieczne. - Refactor (Refaktoryzuj kod)
Kod jest optymalizowany i upraszczany, bez zmieniania jego zachowania. Testy są ponownie uruchamiane, aby upewnić się, że wszystko działa poprawnie po refaktoryzacji.

Zalety TDD
- Dokładne zrozumienie wymagań dokumentacji. Testy piszemy zawsze względem dokumentacji.
- Testy jako dokumentacja jest zawsze aktualna w czasie.
- Testy nie wprowadzają niejednoznaczności, cechy którą może posiadać dokumentacja papierowa.
- Wymuszanie dobrego designu kodu i szybka identyfikacja potencjalnych błędów w designie, np. problem z zależnościami.
- Lepsza zarządzalność kodu w czasie.
- Łatwiejsze i bezpieczniejsze łatanie kodu.
- Natychmiastowy i automatyczny feedback na temat błędu w kodzie.
- Testy regresyjne pozwalają stwierdzić czy po naszych zmianach nie zepsuliśmy przy okazji czegoś w innej części systemu.
- Krótszy, całkowity, czas procesu developmentu.
- Dużo mniej ręcznego debugowania.
Wady TDD
- Czas i wysiłek na trening i przygotowanie developerów.
- Potrzeba dyscypliny osobistej i zespołowej. Testy muszą być zarządzane i poprawiane w czasie w taki sam sposób jak cała reszta kodu.
- Początkowa percepcja dłuższego czasu developmentu.
- Nie wszyscy menadżerowie dają się przekonać. Biją argumentem dwukrotnie dłuższego developmentu, choć całkowity czas trwania developmentu (wliczając szukanie i naprawę błędów, nie tylko pisanie kodu) w TDD jest krótszy niż w nie-TDD.
Test Driven Development (TDD) to metoda tworzenia oprogramowania, która kładzie nacisk na pisanie testów przed kodem produkcyjnym. W idealnym świecie testy jednostkowe powinny być szybkie, niezawodne i niezależne od otoczenia. Jednak w praktyce aplikacje rzadko działają w całkowitej izolacji – korzystają z wielu zależności, takich jak:
- Bazy danych: aplikacja zapisuje lub odczytuje dane.
- API zewnętrzne: wymiana informacji z usługami stron trzecich.
- Usługi wewnętrzne: np. klasy i komponenty komunikujące się ze sobą.
Wszystkie te zależności mogą być trudne do obsłużenia podczas testowania:
- Zależności są wolne: Baza danych lub zewnętrzne API mogą znacząco spowalniać testy.
- Nieprzewidywalne działanie: Zewnętrzne systemy mogą być niestabilne lub niedostępne.
- Efekty uboczne: Testy, które modyfikują dane w bazie lub wykonują rzeczywiste operacje, mogą wpływać na inne testy.
- Skupienie na jednej jednostce kodu: Podczas testowania jednej klasy chcemy skupić się na jej logice, a nie na problemach wynikających z działania jej zależności.
Rozwiązaniem tych problemów są test doubles – specjalne obiekty, które zastępują rzeczywiste zależności podczas testów.

Wyobraźmy sobie system składania zamówień w restauracji, który służy do zarządzania dostępnością dań, obsługą koszyka oraz finalizacją transakcji – posłuży on do zilustrowania każdego rodzaju testu.
W naszym systemie mamy dwa główne komponenty:
- MenuService – odpowiada za dostępność dań w menu.
- OrderService – zarządza zamówieniami (dodawanie pozycji, obliczanie kosztów, finalizacja zamówienia).
Cel testów:
- Sprawdzić, czy koszyk poprawnie oblicza łączny koszt.
- Upewnić się, że tylko dostępne dania mogą zostać dodane do zamówienia.
Dummy
- Służy wyłącznie jako wypełniacz, aby testy mogły się skompilować lub działać.
- Nie zawiera żadnej logiki, a jego metody nigdy nie są wywoływane.
W naszym przykładzie
Nie potrzebujemy żadnych zależności dodatkowych, więc możemy stworzyć pusty obiekt DummyMenuService
, który wypełni wymóg przekazania zależności.
class DummyMenuService implements MenuService {
@Override
public boolean isDishAvailable(String dish) {
return false; // Zwraca domyślną wartość, bo MenuService nie jest potrzebne
}
}
@Test
void shouldInitializeOrderWithEmptyCart() {
MenuService dummyMenuService = new DummyMenuService();
OrderService orderService = new OrderService(dummyMenuService);
assertEquals(0, orderService.getOrderSize()); // Koszyk powinien być pusty na początku
}
Dlaczego Dummy jest potrzebny tutaj?
OrderService
wymaga obiektuMenuService
w konstruktorze.- W tym teście interesuje nas tylko logika koszyka (rozmiar, dodawanie itp.), więc
MenuService
nie musi robić nic konkretnego.
Dla lepszego zrozumienia przedstawmy to na przykładzie z życia codziennego.
Wyobraź sobie, że wchodzisz do restauracji i kelner pyta:
- “Czy masz stolik zarezerwowany?”
Nie masz rezerwacji, ale powiedzmy, że zasady restauracji wymagają wpisania czegoś do rubryki “rezerwacja” (choćby pustego wpisu), zanim cię wpuszczą. W tym przypadku wpisujesz coś nieistotnego, np. “BRAK”. To działa jak Dummy – nie ma znaczenia dla twojego posiłku, ale system rezerwacji nie będzie się na to skarżył.
Stub
- Zastępuje zależność i zwraca z góry określone wartości w odpowiedzi na wywołania metod.
- Używany, gdy wynik zależy od odpowiedzi obiektu, ale nie interesuje nas, jak działa jego implementacja.
W naszym przykładzie
Za pomocą StubMenuService
symulujemy dostępność konkretnych dań w menu. Możemy kontrolować, które dania są dostępne, a które nie.
class StubMenuService implements MenuService {
private final Set<String> availableDishes;
StubMenuService(Set<String> availableDishes) {
this.availableDishes = availableDishes;
}
@Override
public boolean isDishAvailable(String dish) {
return availableDishes.contains(dish); // Zwraca true tylko dla dostępnych dań
}
}
@Test
void shouldAddOnlyAvailableDishesToOrder() {
MenuService stubMenuService = new StubMenuService(Set.of("Pizza", "Burger"));
OrderService orderService = new OrderService(stubMenuService);
orderService.addDish("Pizza"); // Pizza jest dostępna
orderService.addDish("Sushi"); // Sushi nie jest dostępne
assertEquals(1, orderService.getOrderSize()); // Tylko Pizza powinna być w koszyku
}
Mock
- To bardziej zaawansowany test double, który pozwala symulować działanie zależności i weryfikować interakcje z nią.
- Umożliwia sprawdzenie, czy określone metody zostały wywołane z właściwymi argumentami.
W naszym przykładzie
Sprawdzimy interakcję między OrderService
a MenuService
. Chcemy upewnić się, że MenuService
jest wywoływane dla każdego dodawanego dania.
@Test
void shouldCheckDishAvailabilityBeforeAddingToOrder() {
MenuService mockMenuService = Mockito.mock(MenuService.class);
Mockito.when(mockMenuService.isDishAvailable("Pizza")).thenReturn(true);
Mockito.when(mockMenuService.isDishAvailable("Sushi")).thenReturn(false);
OrderService orderService = new OrderService(mockMenuService);
orderService.addDish("Pizza");
orderService.addDish("Sushi");
// Weryfikujemy, że `isDishAvailable` zostało wywołane dwa razy
verify(mockMenuService).isDishAvailable("Pizza");
verify(mockMenuService).isDishAvailable("Sushi");
}
Spy
- Używa rzeczywistego obiektu, ale pozwala monitorować i weryfikować interakcje z nim.
- Przydatny, gdy chcesz testować zachowanie prawdziwego obiektu, ale jednocześnie kontrolować jego interakcje.
W naszym przykładzie
Używamy rzeczywistego MenuService
, które przechowuje dostępne dania, ale chcemy śledzić, które metody zostały wywołane i ile razy.
class RealMenuService implements MenuService {
private final Set<String> availableDishes = new HashSet<>();
void addDish(String dish) {
availableDishes.add(dish);
}
@Override
public boolean isDishAvailable(String dish) {
return availableDishes.contains(dish);
}
}
@Test
void shouldTrackMenuServiceInteractions() {
RealMenuService realMenuService = new RealMenuService();
realMenuService.addDish("Pizza");
MenuService spyMenuService = Mockito.spy(realMenuService);
OrderService orderService = new OrderService(spyMenuService);
orderService.addDish("Pizza");
orderService.addDish("Sushi");
// Weryfikujemy, że metoda `isDishAvailable` została wywołana z odpowiednimi argumentami
verify(spyMenuService).isDishAvailable("Pizza");
verify(spyMenuService).isDishAvailable("Sushi");
}
Fake
- To pełna implementacja zależności, ale uproszczona lub stworzona specjalnie na potrzeby testów.
- Często używana do symulacji bazy danych w pamięci lub innych uproszczonych systemów.
W naszym przykładzie
Tworzymy uproszczoną wersję MenuService
, która działa w pamięci. To rozwiązanie przydatne, gdy potrzebujemy pełnej funkcjonalności w środowisku testowym.
class FakeMenuService implements MenuService {
private final Set<String> availableDishes = new HashSet<>();
void addDish(String dish) {
availableDishes.add(dish);
}
@Override
public boolean isDishAvailable(String dish) {
return availableDishes.contains(dish);
}
}
@Test
void shouldCalculateTotalCostCorrectly() {
FakeMenuService fakeMenuService = new FakeMenuService();
fakeMenuService.addDish("Pizza");
fakeMenuService.addDish("Burger");
OrderService orderService = new OrderService(fakeMenuService);
orderService.addDish("Pizza");
orderService.addDish("Burger");
assertEquals(2, orderService.getOrderSize()); // Powinno być 2 pozycje w zamówieniu
assertEquals(50, orderService.getTotalCost()); // Przyjmujemy, że Pizza i Burger kosztują 25 PLN każda
}
Podsumowanie:
Test doubles pozwalają na elastyczne podejście do testowania złożonych systemów poprzez zastępowanie rzeczywistych zależności odpowiednikami, które są dostosowane do konkretnego scenariusza testowego. W naszym przykładzie składania zamówień w restauracji:
- Dummy pełni rolę “wypełniacza”, używanego wyłącznie po to, aby konstrukcja systemu działała bez błędów, nie wpływając na wynik testu.
- Stub pozwala na kontrolowanie odpowiedzi zależności, symulując dostępność dań w menu.
- Mock umożliwia weryfikację, czy nasz kod wywołuje zależności w oczekiwany sposób.
- Spy monitoruje rzeczywisty obiekt, pozwalając śledzić jego zachowanie w czasie testu.
- Fake dostarcza uproszczoną implementację, która pozwala testować logikę w realistycznych warunkach.
Każdy z tych rodzajów obiektów wspiera izolację testów, umożliwiając skupienie się na konkretnych aspektach działania systemu. Wybór odpowiedniego podejścia zależy od tego, co dokładnie chcemy sprawdzić i jak bardzo zależność jest kluczowa dla testowanej funkcji.