TDD, czyli Test-Driven Development, to sposób pisania kodu, w którym najpierw piszecie jeden mały test opisujący oczekiwane zachowanie, uruchamiacie go i patrzycie, jak nie przechodzi, a dopiero potem dopisujecie kod, który go spełnia, i porządkujecie całość. Cykl powtarza się co kilka minut. W tym artykule przejdziecie go na prawdziwym przykładzie w Javie 21 i JUnit 6, z komunikatami błędów, które sami zobaczyliśmy w terminalu.
Tekst jest dla programistów, testerów automatyzujących i liderów, którzy chcą wprowadzić TDD w zespole bez rewolucji. Najpierw zrozumienie, potem przykład, potem 5 kroków wdrożenia i uczciwa lista sytuacji, w których TDD nie jest najlepszym narzędziem. Jeśli szukacie szerszego kontekstu, czyli gdzie w strategii testów leżą testy jednostkowe, zacznijcie od artykułu o piramidzie testów.
Spis treści
- Czym jest TDD, czyli Test-Driven Development?
- Jak wygląda cykl Red, Green, Refactor?
- Jak wygląda TDD w praktyce? Przykład w Javie krok po kroku
- Dlaczego TDD działa i co mówią badania?
- Czy TDD daje pełne pokrycie kodu testami?
- Jak wdrożyć TDD w zespole w 5 krokach?
- Kiedy TDD się nie sprawdza i co robi w nim tester?
Czym jest TDD, czyli Test-Driven Development?
Test-Driven Development to praktyka programistyczna, w której test powstaje przed kodem produkcyjnym i prowadzi jego projekt. Spopularyzował ją Kent Beck w książce „Test-Driven Development: By Example” z 2002 roku, a w 2023 roku opisał jej kanoniczną wersję w eseju Canon TDD. Martin Fowler w haśle o TDD zwraca uwagę na to, co zespoły najczęściej pomijają: trzecią fazę, czyli refaktoryzację. Bez niej TDD daje działający, ale coraz bardziej splątany kod.
Najważniejsza zmiana nie jest techniczna. W klasycznym podejściu pytanie brzmi „jak to zaimplementować?”, w TDD „po czym poznam, że działa?”. Test staje się pierwszym użytkownikiem Waszej klasy, więc od razu widać, czy jej interfejs jest wygodny. Jeśli test trudno napisać, to zwykle sygnał, że projekt kodu jest zbyt splątany, a nie że test jest zły.
TDD w liczbach
5
kroków Canon TDD według Kenta Becka, 2023
60 do 90%
mniejsza gęstość defektów w 4 zespołach Microsoft i IBM
15 do 35%
dłuższy czas wytworzenia w tych samych zespołach
39
profesjonalistów w badaniu Fucci i in., kolejność testu nie miała znaczenia
Źródło: Kent Beck, Canon TDD, 2023; Microsoft Research, 2009 (badanie Nagappan i in.); Fucci i in., IEEE TSE, 2016
Jak wygląda cykl Red, Green, Refactor?
Cykl ma trzy fazy, a każda ma jedną pracę do wykonania. Red: piszecie test, który nie przechodzi, bo zachowania jeszcze nie ma. Green: dopisujecie najprostszy kod, który ten test spełnia. Refactor: poprawiacie projekt kodu i testów, nie zmieniając zachowania, przy wszystkich testach na zielono. Kent Beck w Canon TDD dodaje przed cyklem listę scenariuszy testowych i wraca do niej po każdym obrocie.
Źródło: Kent Beck, Canon TDD, newsletter.kentbeck.com, 11.12.2023
Ciekawostka, która podważa nazwę metody
Według badania Fucciego, Erdogmusa, Turhana, Oivo i Juristo opublikowanego w IEEE Transactions on Software Engineering w 2016 roku, obejmującego 82 zadania wykonane przez 39 zawodowych programistów, jakość i produktywność wiązały się przede wszystkim z drobnością i równomiernością kroków, a kolejność pisania testu i kodu nie miała istotnego wpływu. Innymi słowy, magia TDD bardziej siedzi w krótkich, regularnych cyklach niż w samym „test najpierw”. Co nie znaczy, że możecie pisać testy w piątek po południu za cały tydzień i nazywać to TDD.
Jak wygląda TDD w praktyce? Przykład w Javie krok po kroku
Weźmy regułę z prawdziwego sklepu: zamówienie od 500 zł dostaje 5 procent rabatu, poniżej 500 zł płacicie pełną kwotę, a ujemna kwota to błąd danych. Każdy krok poniżej skompilowaliśmy javac w trybie Java 21 i uruchomiliśmy w JUnit 6.1.3 10.10.2026. Komunikaty w terminalu są prawdziwe, skrócone tylko o ślad stosu.
Lista testów przed pierwszą linią kodu
Scenariusz 1
Poniżej progu
499,99 zł zostaje 499,99 zł. Wartość tuż pod progiem, bo tu najczęściej mieszka błąd.
Scenariusz 2
Na progu
500,00 zł po rabacie to 475,00 zł. Sprawdza, czy próg jest włącznie.
Scenariusz 3
Dane błędne
Kwota -0,01 zł jest odrzucana wyjątkiem, a nie liczona.
Dopisane w trakcie
Zaokrąglenie
1 234,56 zł po rabacie to 1 172,83 zł, czyli grosze zaokrąglone do dwóch miejsc.
Iteracja 1: Red i Green
Pierwszy test opisuje najprostszy scenariusz. Klasy KalkulatorRabatu jeszcze nie ma, więc czerwone światło zapala się już przy kompilacji. Tak też wygląda Red: test, który nie może przejść, bo zachowania nie ma.
KalkulatorRabatuTest.java, iteracja 1
package pl.qualityisland.tdd;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.math.BigDecimal;
import org.junit.jupiter.api.Test;
class KalkulatorRabatuTest {
private final KalkulatorRabatu kalkulator = new KalkulatorRabatu();
@Test
void zamowieniePonizej500NieMaRabatu() {
assertEquals(new BigDecimal("499.99"), kalkulator.cenaPoRabacie(new BigDecimal("499.99")));
}
}
Terminal, faza Red
KalkulatorRabatuTest.java:10: error: cannot find symbol
private final KalkulatorRabatu kalkulator = new KalkulatorRabatu();
^
symbol: class KalkulatorRabatu
Green oznacza najprostszy kod, który spełnia test. Tutaj to po prostu zwrócenie kwoty. Wygląda naiwnie i taki ma być: nie piszecie logiki, której nie wymaga żaden test.
KalkulatorRabatu.java, iteracja 1
package pl.qualityisland.tdd;
import java.math.BigDecimal;
public class KalkulatorRabatu {
public BigDecimal cenaPoRabacie(BigDecimal kwota) {
return kwota;
}
}
Iteracja 2: rabat na progu i pułapka BigDecimal
Drugi test sprawdza próg. Uruchamiacie go przed zmianą kodu i dostajecie czytelny komunikat: expected: <475.00> but was: <500.00>. Dopisujecie warunek z mnożeniem przez 0,95 i… test nadal jest czerwony.
Terminal, pierwsza próba Green
=> org.opentest4j.AssertionFailedError: expected: <475.00> but was: <475.0000>
Dlaczego 475.00 to nie 475.0000
Według dokumentacji klasy BigDecimal metoda equals uznaje dwie liczby za równe tylko wtedy, gdy mają tę samą wartość i tę samą skalę, więc 475.00 i 475.0000 są dla niej różne, a assertEquals korzysta właśnie z equals. Mnożenie 500.00 przez 0.95 daje skalę 4. To dokładnie ten rodzaj błędu, który w raporcie z produkcji wygląda jak „grosze się nie zgadzają w fakturach”, a w TDD wychodzi w trzeciej minucie pracy.
KalkulatorRabatu.java, iteracja 2, Green
package pl.qualityisland.tdd;
import java.math.BigDecimal;
import java.math.RoundingMode;
public class KalkulatorRabatu {
public BigDecimal cenaPoRabacie(BigDecimal kwota) {
if (kwota.compareTo(new BigDecimal("500")) >= 0) {
return kwota.multiply(new BigDecimal("0.95")).setScale(2, RoundingMode.HALF_UP);
}
return kwota;
}
}
Iteracja 3: dane błędne
Trzeci test żąda wyjątku dla kwoty ujemnej. Red: Expected java.lang.IllegalArgumentException to be thrown, but nothing was thrown. Green: jeden warunek na początku metody. Wszystkie trzy testy zielone.
Refactor: stałe, czytelny warunek, testy parametryzowane
Teraz, przy zielonych testach, porządkujecie kod: magiczne liczby trafiają do stałych z nazwami, warunek brzmi jak reguła biznesowa, a trzy podobne testy zamieniają się w jeden test parametryzowany z tabelą wartości, do której łatwo dopisać kolejny przypadek. Po zmianie uruchamiacie wszystko: 5 testów, 5 zielonych.
KalkulatorRabatu.java, po refaktoryzacji
package pl.qualityisland.tdd;
import java.math.BigDecimal;
import java.math.RoundingMode;
public class KalkulatorRabatu {
static final BigDecimal PROG_RABATU = new BigDecimal("500.00");
static final BigDecimal MNOZNIK_PO_RABACIE = new BigDecimal("0.95");
public BigDecimal cenaPoRabacie(BigDecimal kwota) {
if (kwota.signum() < 0) {
throw new IllegalArgumentException("Kwota nie może być ujemna: " + kwota);
}
if (kwota.compareTo(PROG_RABATU) < 0) {
return kwota;
}
return kwota.multiply(MNOZNIK_PO_RABACIE).setScale(2, RoundingMode.HALF_UP);
}
}
KalkulatorRabatuTest.java, po refaktoryzacji
package pl.qualityisland.tdd;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.math.BigDecimal;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
class KalkulatorRabatuTest {
private final KalkulatorRabatu kalkulator = new KalkulatorRabatu();
@ParameterizedTest(name = "{0} zł po rabacie to {1} zł")
@CsvSource({
"0.00, 0.00",
"499.99, 499.99",
"500.00, 475.00",
"1234.56, 1172.83"
})
void cenaPoRabacie(String kwota, String oczekiwana) {
assertEquals(new BigDecimal(oczekiwana), kalkulator.cenaPoRabacie(new BigDecimal(kwota)));
}
@Test
void ujemnaKwotaJestOdrzucana() {
assertThrows(IllegalArgumentException.class,
() -> kalkulator.cenaPoRabacie(new BigDecimal("-0.01")));
}
}
Zwróćcie uwagę na dwie rzeczy. Wartości brzegowe 499,99 i 500,00 pochodzą z listy testów, nie z wyobraźni w ostatniej chwili, i to one zabiją typową pomyłkę > zamiast >=. Po drugie, kod produkcyjny ma kilkanaście linii i powstał w trzech obrotach cyklu, a każdy obrót trwał kilka minut. Techniki doboru takich wartości, czyli podział na klasy równoważności i analiza wartości brzegowych, ćwiczymy na szkoleniu testy jednostkowe dla testerów.
Dlaczego TDD działa i co mówią badania?
TDD działa, bo zmusza do zrozumienia wymagań przed pisaniem kodu i daje informację zwrotną w minutach, a nie po tygodniu w środowisku testowym. Dzięki temu:
- niejasne wymaganie wychodzi przy pisaniu pierwszego testu, a nie na demo u klienta: w naszym przykładzie pytanie „czy 500 zł to już rabat?” musiało paść przed implementacją,
- każdy fragment kodu ma od początku przynajmniej jednego użytkownika, czyli test, więc interfejsy klas są prostsze,
- błąd wprowadzony przy zmianie widać po kilku sekundach, w małym kroku, więc wiadomo, która linia go spowodowała,
- testy zostają jako wykonywalna dokumentacja: tabela z testu parametryzowanego mówi więcej o regule rabatu niż komentarz w kodzie,
- refaktoryzacja przestaje być ryzykiem, bo siatka testów od razu pokazuje zmianę zachowania.
Najczęściej cytowane dane pochodzą z badania Nagappana, Maximiliena, Bhata i Williams na 3 zespołach Microsoftu i 1 zespole IBM. Według podsumowania Microsoft Research z 2009 roku zespoły stosujące TDD miały gęstość defektów lepszą o 60 do 90 procent od porównywalnych zespołów bez TDD, ale projekty trwały o 15 do 35 procent dłużej. To uczciwy rachunek: płacicie czasem na początku, odzyskujecie go na utrzymaniu i regresji.
Źródło: zestawienie Quality Island na podstawie źródeł z listy na końcu artykułu
Czy TDD daje pełne pokrycie kodu testami?
Nie, i warto to powiedzieć wprost, bo w wielu tekstach o TDD pada obietnica „niemal stuprocentowego pokrycia”. TDD zwykle daje wysokie pokrycie nowego kodu, bo każda linia powstaje w odpowiedzi na test. Ale pokrycie zależy od praktyki zespołu: od tego, czy lista testów obejmuje przypadki brzegowe i błędne, czy zespół pisze test do każdej poprawki błędu i czy kod starszy niż TDD w ogóle ma testy. Autorzy wpisu Code Coverage Best Practices z Google piszą wprost, że wysoki procent pokrycia nie gwarantuje wysokiej jakości testów, a gonienie stu procent daje fałszywe poczucie bezpieczeństwa.
Dobrym przykładem jest nasz kalkulator. Gdyby lista testów zawierała tylko 1 000 zł i 100 zł, pokrycie linii wynosiłoby 100 procent po dopisaniu testu ujemnej kwoty, a pomyłka > zamiast >= na progu przeszłaby niezauważona. Pokrycie mówi, które linie się wykonały, nie mówi, czy sprawdziliście właściwe wartości. Dlatego w naszych projektach przy przeglądzie testów jednostkowych zamiast samego procentu oglądamy listę przypadków brzegowych i to, czy asercje sprawdzają wynik, a nie tylko brak wyjątku.
Jak wdrożyć TDD w zespole w 5 krokach?
TDD wdraża się zmianą nawyków, nie zarządzeniem. Pięć kroków poniżej układa tę zmianę w kolejności, która u naszych klientów najczęściej kończy się tym, że zespół zostaje przy TDD po pierwszym miesiącu, a nie wraca do „testy dopiszemy później”.
Krok 1
Zrozumcie cykl na katach
Dwie lub trzy sesje po 60 do 90 minut na prostych ćwiczeniach (FizzBuzz, kalkulator rabatu, walidacja numeru konta) w parach. Cel: poczuć rytm Red, Green, Refactor bez presji kodu produkcyjnego.
Krok 2
Przygotujcie narzędzia
Framework testów dla Waszego języka: JUnit 6 dla Javy, pytest dla Pythona, Vitest albo Jest dla TypeScriptu, xUnit albo NUnit dla C#. Testy uruchamiane jednym skrótem w IDE i automatycznie w CI przy każdym pull requeście.
Krok 3
Zacznijcie od nowego kodu
Pierwsze tygodnie tylko na nowych, dobrze odizolowanych modułach z logiką: reguły cenowe, walidacje, obliczenia. Kod z bazą danych i interfejsem użytkownika zostawcie na później.
Krok 4
Pilnujcie cyklu i listy testów
Lista scenariuszy w opisie zadania, jeden test naraz, refaktoryzacja tylko na zielonym. Krótkie sesje pracy w parach albo mob programming pomagają bardziej niż dokument z zasadami.
Krok 5
Mierzcie i rozmawiajcie
Co sprint: liczba defektów znalezionych po merge, czas od zgłoszenia do poprawki, czas trwania testów jednostkowych. Retrospektywa z pytaniem, gdzie TDD pomogło, a gdzie przeszkadzało.
Najwięcej zespołów potyka się na kroku 3. Próba wprowadzenia TDD od razu w starym kodzie bez testów kończy się frustracją, bo klasa z dziesięcioma zależnościami nie da się łatwo przetestować w izolacji. Wtedy lepszy pierwszy ruch to testy charakteryzujące obecne zachowanie i stopniowe wydzielanie logiki. O tym, jak wpleść jakość w pracę zespołu zwinnego bez spowalniania dostaw, piszemy też na Strefie QA w tekście Jak wdrożyć QA w zespole agile bez spowalniania developmentu.
Jeśli chcecie przejść kroki 1 i 4 z trenerem na Waszym stosie technologicznym, prowadzimy szkolenie testy jednostkowe i integracyjne dla programistów, a gdy potrzebny jest plan dla całej organizacji, pomagamy w ramach doradztwa w automatyzacji testów. Według danych Quality Island przeszkoliliśmy już ponad 2 500 specjalistów, a ankiety po szkoleniach (ponad 350 wypełnionych) dają 5 na 5 za poziom merytoryczny.
Kiedy TDD się nie sprawdza i co robi w nim tester?
TDD nie jest odpowiedzią na wszystko i dobrze jest znać jego granice, zanim zespół zacznie się z nim kłócić.
Źródło: doświadczenie Quality Island z przeglądów testów jednostkowych
A gdzie w tym wszystkim tester? Bliżej, niż się wydaje. Lista testów z kroku zero to dokładnie to, w czym testerzy są najlepsi: przypadki brzegowe, dane błędne, kombinacje warunków. W zespołach, które szkolimy, dobrze działa prosta zasada: przed zadaniem z logiką biznesową programista i tester przez 10 minut spisują razem listę scenariuszy. Jeśli scenariusze opisujecie językiem biznesu, naturalnym kolejnym krokiem jest BDD i składnia Gherkin, o której piszemy w artykule język Gherkin: co to jest i jak go używać, a praktykę ćwiczymy na szkoleniu Behavior-Driven Development w praktyce. Na wyższych poziomach testów, gdzie TDD nie sięga, przydają się testy integracyjne.
Co zabrać z tego artykułu
01TDD to cykl Red, Green, Refactor poprzedzony listą testów; Kent Beck w Canon TDD z 2023 roku opisuje go w 5 krokach.
02Najcenniejsze w TDD są małe, regularne kroki: badanie Fucci i in. z 2016 roku nie znalazło istotnego wpływu samej kolejności test i kod.
03Prawdziwy przykład pokazał błąd, którego nikt nie planował: BigDecimal 475.00 to dla equals nie to samo co 475.0000.
04TDD nie gwarantuje pełnego pokrycia; pokrycie zależy od listy przypadków brzegowych i dyscypliny zespołu, a Google traktuje 60, 75 i 90 procent jako wytyczne, nie cel.
05Wdrożenie zacznijcie od kat i nowego kodu z logiką, pilnujcie cyklu w parach i mierzcie defekty po merge, zamiast liczyć tylko procent pokrycia.
Źródło: Kent Beck 2023, Fucci i in. 2016, Microsoft Research 2009, Google Testing Blog 2020, przykład Quality Island 10.10.2026
Chcecie wprowadzić TDD w zespole, ale nie wiecie, od którego modułu zacząć? Przejrzymy Wasze testy jednostkowe i przygotujemy plan wdrożenia na Waszym kodzie.
Powiązane na blogu Quality Island
- Piramida testów i techniki projektowania testów
- Shift left testing: wady i zalety
- Koszty i oszczędności testowania: dlaczego warto testować na wczesnym etapie
- Język Gherkin: co to jest i jak go używać
- Co to są testy integracyjne
Pełna lista źródeł
- Kent Beck, Canon TDD, 11.12.2023. Stąd 5 kroków i lista typowych błędów.
- Martin Fowler, Test Driven Development, bliki, 2005, aktualizacja 11.12.2023.
- D. Fucci, H. Erdogmus, B. Turhan, M. Oivo, N. Juristo, A Dissection of the Test-Driven Development Process: Does It Really Matter to Test-First or to Test-Last?, IEEE Transactions on Software Engineering, 2016.
- Microsoft Research, Exploding Software-Engineering Myths, 2009, omówienie badania N. Nagappan, E. M. Maximilien, T. Bhat, L. Williams.
- C. Arguelles, M. Ivanković, A. Bender, Code Coverage Best Practices, Google Testing Blog, 2020.
- Oracle, BigDecimal, Java SE 21 API, 2023. Stąd zasada equals ze skalą.
- JUnit, JUnit 6, wydanie 6.1.3, 2026.
- Quality Island, przykład kalkulatora rabatu skompilowany w Javie 21 i uruchomiony w JUnit 6.1.3, 10.10.2026; liczba przeszkolonych i wyniki ankiet z danych firmy, 2026.
Autor: Tomasz Stelmach, Quality Island.