TDD- po co jest potrzebny ?

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
Automatyzacja
Refaktoryzacja
Narzędzia do śledzenia postępów
STUB
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.// 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:
// 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
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
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.