Piramida testów – czy to jeszcze ma sens?
W poprzednim wpisie opisałem piramidę testów jako fundament strategii testowania. Dziś chcę zadać trudniejsze pytanie: czy w 2021 roku piramida testów nadal ma sens? A może zmieniła się rzeczywistość, w której pracujemy?
Co zmieniło się od czasu powstania piramidy?
Piramida testów pochodzi z czasów, gdy testy integracyjne były naprawdę drogie. Uruchomienie bazy danych wymagało prawdziwego serwera. Testy działające przeciwko infrastrukturze były powolne, niestabilne i trudne do skonfigurowania w CI.
Dziś jest inaczej.
Docker i TestContainers obniżyły cenę testów integracyjnych
TestContainers to biblioteka, która pozwala uruchomić kontenery Dockerowe bezpośrednio z poziomu testu JUnit:
@Testcontainers
class OrderServiceIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:14");
@Test
void shouldPersistOrder() {
// Test działa przeciwko prawdziwej bazie PostgreSQL
// uruchomionej w kontenerze Docker
}
}
Koszt takiego testu wzrósł — kilka sekund zamiast milisekund — ale zysk jest ogromny: testujesz dokładnie to, co trafi na produkcję.
Frameworki generują więcej kodu za nas
Jeśli korzystasz z Spring Data i piszesz tylko:
public interface OrderRepository extends JpaRepository<Order, Long> {
List<Order> findByCustomerIdAndStatus(Long customerId, OrderStatus status);
}
...to co właściwie testujesz w teście jednostkowym? Framework implementuje tę metodę za Ciebie. Test jednostkowy sprawdziłby wyłącznie to, że wywołałeś metodę repozytorium — co jest testem niczego.
Tutaj sens mają testy integracyjne: sprawdzają, czy Spring Data wygenerował poprawne zapytanie SQL i czy zwraca właściwe dane.
Obsesja na punkcie code coverage
Wiele zespołów wpadło w pułapkę: "mamy 80% coverage, jesteśmy bezpieczni". Problem w tym, że coverage mierzy, które linie kodu zostały wykonane — nie to, czy testy faktycznie weryfikują poprawne zachowanie.
Można mieć 100% coverage z testami, które nie sprawdzają żadnych asercji. Coverage to metryka pomocnicza, nie cel sam w sobie.
Moduły głębokie vs płytkie
John Ousterhout w "A Philosophy of Software Design" wprowadza pojęcie modułów głębokich i płytkich:
- Moduł głęboki — małe, proste API ukrywające bogatą, złożoną implementację
- Moduł płytki — skomplikowane API przy prostej implementacji
Testy jednostkowe najlepiej sprawdzają się przy modułach głębokich — bogatej logice biznesowej, algorytmach, transformacjach danych.
Przy modułach płytkich (CRUD, mapowanie DTO, proste endpointy REST) testy jednostkowe często testują tylko konfigurację frameworka — a to lepiej robi test integracyjny.
Odwrócona piramida — kiedy ma sens?
Martin Fowler opisuje "odwróconą piramidę" (lub "lody") jako antypattern: dużo testów E2E, mało jednostkowych. Jest to zazwyczaj efekt braku kultury testowania i nadrabiania zaległości testami systemowymi.
Ale są konteksty, gdzie więcej testów integracyjnych niż jednostkowych jest uzasadnione:
- Aplikacje CRUD z prostą logiką biznesową
- Projekty intensywnie używające frameworków (Spring Data, Hibernate)
- Systemy z dużą liczbą integracji z zewnętrznymi serwisami
W takich przypadkach "puchar" (dużo integracyjnych, mało jednostkowych) może być lepszą metaforą niż piramida.
Koszty różnych poziomów testów
Warto spojrzeć na koszt całkowity, nie tylko czas wykonania:
| Typ testu | Czas wykonania | Koszt pisania | Koszt utrzymania | Wartość informacyjna | |-----------|---------------|---------------|------------------|---------------------| | Jednostkowy | ms | niski | niski | wysoka (dla logiki) | | Integracyjny | sekundy | średni | średni | wysoka (dla integracji) | | E2E | minuty | wysoki | wysoki | wysoka (dla przepływów) |
Wnioski: elastyczność zamiast dogmatyzmu
Piramida testów pozostaje użytecznym punktem wyjścia, ale nie powinna być traktowana dogmatycznie. Kilka praktycznych wniosków:
1. Dostosuj strategię do charakteru kodu
- Bogata logika biznesowa → więcej testów jednostkowych
- Kod integracyjny, CRUD → więcej testów integracyjnych
- Krytyczne przepływy użytkownika → testy E2E
2. Używaj TestContainers bez strachu Docker znormalizował testy integracyjne. Nie bój się ich pisać.
3. Unikaj testów, które niczego nie testują Test, który tylko weryfikuje, że wywołałeś mocked metodę, nie daje żadnej wartości. Pytaj: "co zepsuje się, jeśli ten test nie przejdzie?"
4. Patrz na całkowity czas suite'u, nie poszczególnych testów Jeśli masz 1000 testów jednostkowych (1s łącznie) i 100 testów integracyjnych (3 minuty łącznie) — czas integracyjnych dominuje. Warto inwestować w ich optymalizację.
5. Nie myl coverage z jakością Dobry test to taki, który przyłapuje błędy — nie taki, który tylko zwiększa coverage.
Piramida testów daje nam język do rozmowy o strategii testowania. Ale ostateczna decyzja zawsze powinna wynikać z rozumienia kontekstu projektu, a nie ze ślepego podążania za metaforą trójkąta.