Test Driven Development

Test Driven Development (TDD) to metodyka programowania, w której testy są tworzone przed właściwą implementacją kodu. Proces składa się z trzech kroków:

  1. Pisanie testu – tworzenie testu sprawdzającego konkretną, minimalną funkcjonalność.
  2. Implementacja kodu – pisanie minimalnego kodu potrzebny do przejścia testu.
  3. Refaktoryzacja – poprawa i optymalizacja kodu, zachowując jego działanie.

Dzięki TDD kod jest lepiej strukturyzowany, mniej podatny na błędy i łatwiejszy w utrzymaniu.

Kolory na powyższym diagramie nie są przypadkowe. Kolor czerwony kojarzy się z nieprzechodzącym testem, zielony natomiast z udanym 🙂

STUB

Stub to prosta, „podstawiona” implementacja klasy lub metody, która zwraca określone wartości bez rzeczywistej logiki. Stuby są używane do izolowania testowanego kodu od zależności, które mogą być trudne do kontrolowania (np. baza danych, API).

public interface UserRepository {
    User getUserById(int id);
}

public class UserRepositoryStub implements UserRepository {
    @Override
    public User getUserById(int id) {
        return new User(id, "Testowy Użytkownik");
    }
}

public class UserService {
    private UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public String getUserName(int id) {
        return userRepository.getUserById(id).getName();
    }
}

// Test:
public class UserServiceTest {
    public static void main(String[] args) {
        UserRepositoryStub stub = new UserRepositoryStub();
        UserService userService = new UserService(stub);

        System.out.println(userService.getUserName(1)); // Powinno zwrócić "Testowy Użytkownik"
    }
}

Spy

Spy to specjalny rodzaj testowego obiektu, który nie tylko podaje ustalone wartości (jak Stub), ale także rejestruje interakcje z testowanym kodem. Dzięki temu możemy sprawdzić, czy metody były wywoływane zgodnie z oczekiwaniami. Spy jest przydatne, gdy chcemy sprawdzić czy i jak kod korzysta z danej zależności.

public interface Notifier {
    void sendNotification(String message);
}

public class NotifierSpy implements Notifier {
    private boolean wasCalled = false;
    private String lastMessage = "";

    @Override
    public void sendNotification(String message) {
        wasCalled = true;
        lastMessage = message;
    }

    public boolean wasSendNotificationCalled() {
        return wasCalled;
    }

    public String getLastMessage() {
        return lastMessage;
    }
}

public class NotificationService {
    private Notifier notifier;

    public NotificationService(Notifier notifier) {
        this.notifier = notifier;
    }

    public void notifyUser(String message) {
        notifier.sendNotification(message);
    }
}

// Test:
public class NotificationServiceTest {
    public static void main(String[] args) {
        NotifierSpy spy = new NotifierSpy();
        NotificationService service = new NotificationService(spy);

        service.notifyUser("Testowe powiadomienie");

        System.out.println("Czy metoda została wywołana? " + spy.wasSendNotificationCalled()); // true
        System.out.println("Ostatnia wiadomość: " + spy.getLastMessage()); // "Testowe powiadomienie"
    }
}

Mock

Mock to obiekt testowy, który symuluje zachowanie rzeczywistej zależności i pozwala na weryfikację interakcji – czyli sprawdzanie, czy metody zostały wywołane i z jakimi argumentami. Mocki są świetne do testowania zachowania kodu, zamiast jego faktycznych wyników.

import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.*;

public class UserServiceTest {
    
    @Test
    public void testGetUserName() {
        // Tworzymy mock obiektu UserRepository
        UserRepository mockRepo = mock(UserRepository.class);

        // Definiujemy zachowanie mocka – zwróci konkretnego użytkownika przy wywołaniu getUserById
        when(mockRepo.getUserById(1)).thenReturn(new User(1, "Mockowany Użytkownik"));

        // Tworzymy instancję serwisu z użyciem mocka
        UserService userService = new UserService(mockRepo);

        // Wywołujemy metodę i sprawdzamy wynik
        String userName = userService.getUserName(1);
        assertEquals("Mockowany Użytkownik", userName);

        // Weryfikujemy, czy metoda getUserById została wywołana dokładnie raz z argumentem 1
        verify(mockRepo, times(1)).getUserById(1);
    }
}