Test Driven Development to praktyka programistyczna, w której najpierw pisane są testy jednostkowe, a następnie implementacja kodu, aby te testy przeszły. TDD obejmuje kilka kluczowych elementów i wymaga pewnych narzędzi i praktyk.

Framework do testów

Do wykonywania testów jednostkowych potrzebujesz odpowiedniego frameworka, który umożliwia definiowanie i uruchamianie testów. W przypadku języków programowania takich jak Java, Python, czy JavaScript popularne są odpowiednio JUnit, PyTest, i Jasmine.

Automatyzacja

Testy jednostkowe powinny być łatwe do automatyzacji, co oznacza, że ​​musisz móc uruchomić je w sposób zautomatyzowany, na przykład przy użyciu skryptów budujących (np. Maven, Gradle, npm) lub narzędzi CI/CD (Continuous Integration/Continuous Deployment), takich jak Jenkins, Travis CI czy GitHub Actions.

Refaktoryzacja

TDD zachęca do częstego refaktoryzowania kodu, czyli poprawiania jego struktury bez zmiany jego funkcji. Narzędzia do refaktoryzacji, dostępne w większości nowoczesnych edytorów i IDE (Integrated Development Environment), mogą ułatwić ten proces.

Narzędzia do śledzenia postępów

TDD zachęca do częstego refaktoryzowania kodu, czyli poprawiania jego struktury bez zmiany jego funkcji. Narzędzia do refaktoryzacji, dostępne w większości nowoczesnych edytorów i IDE (Integrated Development Environment), mogą ułatwić ten proces.

STUB

W TDD, tworzenie „stubów” jest często używane do izolowania kodu od zależności zewnętrznych i koncentrowania się na testowaniu jednostkowym bez konieczności interakcji z pełnymi implementacjami tych zależności.

W przykładzie zakładamy, że mamy prostą klasę ShoppingCart, która korzysta z interfejsu PriceCalculatorService do obliczania sumy zakupów. W celu izolacji od rzeczywistego serwisu, będziemy stosować Stub w testach jednostkowych.
Java
// PriceCalculatorService.java
public interface PriceCalculatorService {
    double calculatePrice(String item, int quantity);
}

// ShoppingCart.java
public class ShoppingCart {
    private List<Map<String, Object>> items = new ArrayList<>();
    private PriceCalculatorService priceCalculatorService;

    public ShoppingCart(PriceCalculatorService priceCalculatorService) {
        this.priceCalculatorService = priceCalculatorService;
    }

    public void addItem(String item, int quantity) {
        Map<String, Object> newItem = new HashMap<>();
        newItem.put("item", item);
        newItem.put("quantity", quantity);
        items.add(newItem);
    }

    public double calculateTotalPrice() {
        double totalPrice = 0;
        for (Map<String, Object> item : items) {
            String itemName = (String) item.get("item");
            int quantity = (int) item.get("quantity");
            totalPrice += priceCalculatorService.calculatePrice(itemName, quantity);
        }
        return totalPrice;
    }
}

Teraz napiszemy testy jednostkowe z użyciem Stub dla klasy ShoppingCart:

Java
// ShoppingCartTest.java
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.*;

class ShoppingCartTest {

    @Test
    void testCalculateTotalPriceWithStub() {
        // Tworzymy mock (stub) dla interfejsu PriceCalculatorService
        PriceCalculatorService priceCalculatorStub = mock(PriceCalculatorService.class);
        when(priceCalculatorStub.calculatePrice(anyString(), anyInt())).thenReturn(10.0);

        // Tworzymy koszyk zakupowy, podając mu naszego mocka
        ShoppingCart cart = new ShoppingCart(priceCalculatorStub);

        // Dodajemy przedmioty do koszyka (możemy dodać cokolwiek, bo mock zawsze zwróci 10.0)
        cart.addItem("Product A", 2);
        cart.addItem("Product B", 3);

        // Wywołujemy metodę, którą chcemy przetestować
        double totalPrice = cart.calculateTotalPrice();

        // Sprawdzamy, czy metoda korzysta z naszego mocka i sumuje ceny poprawnie
        assertEquals(50.0, totalPrice, 0.01);
    }
}

W tym przykładzie, priceCalculatorStub jest „stubem” stworzonym przy użyciu Mockito. Mock ten zawsze zwraca wartość 10.0, niezależnie od przekazanych mu argumentów. W rzeczywistych przypadkach, Stub mógłby reagować na różne wejścia w różny sposób, aby przetestować różne scenariusze. Stosując takie zastosowanie Stub, możemy przetestować funkcjonalność koszyka zakupowego, izolując go od rzeczywistego serwisu do obliczania cen.

Spy

Rodzaj obiektu, który jest używany do śledzenia i raportowania na temat wywołań metod na rzeczywistym obiekcie. Często stosuje się go, gdy chcemy przetestować, czy pewne metody zostały wywołane z odpowiednimi argumentami.
Java
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.*;

class ExampleService {
    public String performAction(String input) {
        // Jakaś implementacja...
        return "Result: " + input;
    }
}

class ExampleClient {
    private ExampleService service;

    public ExampleClient(ExampleService service) {
        this.service = service;
    }

    public String doSomethingWithService(String data) {
        // Wywołujemy metodę na obiekcie ExampleService
        String result = service.performAction(data);
        // Przetwarzamy wynik...
        return "Processed: " + result;
    }
}

class ExampleTest {

    @Test
    void testExampleClientWithSpy() {
        // Tworzymy spy dla rzeczywistego obiektu ExampleService
        ExampleService realService = new ExampleService();
        ExampleService spyService = spy(realService);

        // Tworzymy obiekt ExampleClient i podstawiamy go spyService
        ExampleClient client = new ExampleClient(spyService);

        // Wywołujemy metodę na ExampleClient
        String result = client.doSomethingWithService("InputData");

        // Sprawdzamy, czy metoda performAction na spyService została poprawnie wywołana
        verify(spyService).performAction("InputData");

        // Sprawdzamy oczekiwany wynik
        assertEquals("Processed: Result: InputData", result);
    }
}

W tym przykładzie, spyService jest obiektem typu ExampleService, ale działa on również jako Spy dla rzeczywistego obiektu realService. Dzięki temu możemy sprawdzić, czy metoda performAction została wywołana z odpowiednimi argumentami, jednocześnie zachowując normalne zachowanie tej metody.

Funkcja verify(spyService).performAction("InputData") sprawdza, czy metoda performAction została wywołana z argumentem „InputData”. Jeśli tak, test zostanie uznany za udany. To podejście pozwala na sprawdzenie interakcji między obiektami i na przetestowanie, czy pewne metody są wywoływane z oczekiwanymi parametrami.

Mock

Obiekt, który jest używany do zastępowania rzeczywistych implementacji interfejsów lub klas. Mocki pozwalają na kontrolowanie zachowania testowanego kodu i śledzenie, czy pewne metody zostały wywołane, z jakimi argumentami, ile razy, itp. W języku Java, jednym z popularnych frameworków do tworzenia mocków jest Mockito.
Java
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.*;

// Klasa reprezentująca serwis
class ExampleService {
    public String performAction(String input) {
        // Jakaś implementacja...
        return "Result: " + input;
    }
}

// Klasa kliencka korzystająca z serwisu
class ExampleClient {
    private ExampleService service;

    public ExampleClient(ExampleService service) {
        this.service = service;
    }

    public String doSomethingWithService(String data) {
        // Wywołujemy metodę na obiekcie ExampleService
        String result = service.performAction(data);
        // Przetwarzamy wynik...
        return "Processed: " + result;
    }
}

// Klasa testowa
class ExampleTest {

    @Test
    void testExampleClientWithMock() {
        // Tworzymy mock dla interfejsu lub klasy ExampleService
        ExampleService mockService = mock(ExampleService.class);

        // Określamy, jak ma się zachować mock, gdy zostanie wywołana metoda performAction
        when(mockService.performAction("InputData")).thenReturn("MockedResult: InputData");

        // Tworzymy obiekt ExampleClient i podstawiamy go mockService
        ExampleClient client = new ExampleClient(mockService);

        // Wywołujemy metodę na ExampleClient
        String result = client.doSomethingWithService("InputData");

        // Sprawdzamy, czy metoda performAction została wywołana z oczekiwanym argumentem
        verify(mockService).performAction("InputData");

        // Sprawdzamy oczekiwany wynik
        assertEquals("Processed: MockedResult: InputData", result);
    }
}

W tym przykładzie, mockService jest mockiem obiektu typu ExampleService. Metoda when(mockService.performAction("InputData")).thenReturn("MockedResult: InputData") określa, jak mock ma się zachować, gdy zostanie wywołana metoda performAction z argumentem „InputData”. W naszym przypadku, mock zwraca „MockedResult: InputData”. Później w asercji verify(mockService).performAction("InputData") sprawdzamy, czy metoda performAction została wywołana z oczekiwanym argumentem.

Mocki są przydatne, gdy chcemy skupić się na testowaniu konkretnego fragmentu kodu, izolując go od rzeczywistych implementacji zależności. Warto jednak używać ich z umiarem, aby nie utrudnić zrozumienia testów czy kodu.

Scroll to Top