TDD (test-driven development) to technika pisania kodu: najpierw piszesz test, który się nie udaje, potem najprostszy kod, który go naprawia, a na końcu porządkujesz całość (cykl red-green-refactor). BDD (behavior-driven development) opisuje to samo z perspektywy zachowania systemu, w zrozumiałych dla wszystkich scenariuszach „Given / When / Then”. TDD sprawdza, czy kod jest zbudowany poprawnie, a BDD, czy budujesz to, czego ktoś naprawdę potrzebuje, więc najlepiej stosować je razem.
Skróty TDD i BDD pojawiają się w ofertach pracy, na rozmowach rekrutacyjnych i w dyskusjach o tym, jak sprawdzać kod napisany przez AI. Często występują razem, więc łatwo uznać je za dwie nazwy tej samej rzeczy. To nie to samo, ale też nie konkurencja: TDD pilnuje poprawności kodu, BDD pilnuje zgodności z tym, czego ktoś potrzebuje. Pokażę oba podejścia na jednym przykładzie z artykułu o spec-driven development: zapisie adresu e-mail na newsletter. Cały kod uruchomiłem lokalnie, a wyniki poniżej pochodzą z tego uruchomienia.
Czym jest TDD i na czym polega cykl red-green-refactor?
TDD (test-driven development) to technika, w której test powstaje przed kodem, który ma go spełnić. Pracujesz w krótkim, powtarzalnym cyklu: czerwony, zielony, refaktoryzacja (red-green-refactor).
| Krok | Co robisz | Jak wygląda wynik |
|---|---|---|
| Red (czerwony) | Piszesz test opisujący jedno zachowanie, którego jeszcze nie ma | Test nie przechodzi, i to z właściwego powodu |
| Green (zielony) | Piszesz najprostszy kod, który spełnia test | Wszystkie testy przechodzą |
| Refactor (porządki) | Poprawiasz nazwy i strukturę, nie zmieniając zachowania | Testy nadal przechodzą |
Najważniejszy jest krok pierwszy. Zobaczenie, że test najpierw się nie udaje, daje pewność, że naprawdę coś sprawdza. Test, który nigdy nie był czerwony, może przechodzić zawsze, także wtedy, gdy kod jest zły.
Jak wygląda TDD w pytest krok po kroku?
Wymagania pochodzą z kryteriów akceptacji: poprawny e-mail zostaje zapisany, niepoprawny jest odrzucony, duplikat nie tworzy drugiego wpisu. Zaczynamy od pierwszego z nich. Najpierw test, w pliku test_newsletter.py:
from newsletter import subscribe
def test_valid_email_is_saved():
subscribers = set()
result = subscribe("ala@example.com", subscribers)
assert result == "created"
assert subscribers == {"ala@example.com"}
Red. Uruchamiasz pytest, a plik newsletter.py jeszcze nie istnieje:
ModuleNotFoundError: No module named 'newsletter'
1 error in 0.04s
Green. Piszesz najprostszy kod, jaki spełnia test (nie „idealny”, tylko najprostszy):
def subscribe(email, subscribers):
subscribers.add(email)
return "created"
1 passed in 0.01s
Drugi cykl: niepoprawny adres. Dopisujesz test (na górze pliku dochodzi import pytest):
def test_invalid_email_is_rejected():
subscribers = set()
with pytest.raises(ValueError):
subscribe("ala.example.com", subscribers)
assert subscribers == set()
Test jest czerwony, bo obecny kod przyjmuje każdy tekst:
E Failed: DID NOT RAISE ValueError
1 failed, 1 passed in 0.01s
Dodajesz walidację:
def subscribe(email, subscribers):
if "@" not in email:
raise ValueError("Niepoprawny adres e-mail")
subscribers.add(email)
return "created"
2 passed in 0.00s
Trzeci cykl: duplikat. Test:
def test_duplicate_email_is_not_saved_twice():
subscribers = {"ala@example.com"}
result = subscribe("ala@example.com", subscribers)
assert result == "duplicate"
assert len(subscribers) == 1
Czerwony wynik mówi dokładnie, czego brakuje:
E AssertionError: assert 'created' == 'duplicate'
1 failed, 2 passed in 0.01s
Po dopisaniu warunku wszystko przechodzi (3 passed). Na koniec refactor: wyciągasz sprawdzanie adresu do osobnej funkcji, żeby kolejne reguły (np. dłuższa walidacja) miały swoje miejsce, a testy pilnują, że zachowanie się nie zmieniło:
def is_valid_email(email):
return "@" in email
def subscribe(email, subscribers):
if not is_valid_email(email):
raise ValueError("Niepoprawny adres e-mail")
if email in subscribers:
return "duplicate"
subscribers.add(email)
return "created"
3 passed in 0.00s
Dwie rzeczy warto zauważyć. Każdy test opisuje jedno zachowanie i ma czytelną nazwę, więc przy czerwonym wyniku od razu wiesz, co się zepsuło. A kod powstawał małymi krokami, bez „na zapas” dopisanych funkcji.
Czym jest BDD i czym różni się od TDD?
BDD (behavior-driven development) wywodzi się z TDD, ale zmienia punkt widzenia: zamiast testować funkcje, opisujesz zachowanie systemu własnymi słowami, w formie scenariuszy, które rozumie także osoba nietechniczna. Scenariusz ma trzy części (składnia Gherkin): Given (stan wyjściowy), When (akcja), Then (oczekiwany rezultat).
| TDD | BDD | |
|---|---|---|
| Pytanie | Czy zbudowałem to poprawnie? | Czy zbudowałem to, czego potrzeba? |
| Kto czyta testy | Programista | Programista, tester, osoba od produktu |
| Język | Kod (assert) | Zdania w języku zbliżonym do naturalnego (Given / When / Then) |
| Poziom | Funkcja, klasa | Zachowanie widoczne dla użytkownika |
| Skąd się bierze | Decyzja programisty | Kryteria akceptacji (acceptance criteria) |
| Typowe narzędzia w Pythonie | pytest | pytest-bdd, behave |
Ta różnica wyjaśnia pytanie, które regularnie wraca na Stack Overflow (bez zadowalającej odpowiedzi): czy TDD to „weryfikacja”, a BDD „walidacja”. Prościej: TDD odpowiada na pytanie „czy ta funkcja robi to, co zamierzałem?”, a BDD na pytanie „czy to, co zamierzałem, jest tym, o co poprosił użytkownik?”. Można mieć kod w pełni przetestowany w TDD, który rozwiązuje zły problem.
Jak zapisać te same wymagania w BDD (Given / When / Then)?
Kryteria akceptacji z naszego przykładu zamieniają się w scenariusze prawie jeden do jednego. Plik features/newsletter.feature:
Feature: Zapis na newsletter
Scenario: Poprawny adres e-mail
Given lista subskrybentów jest pusta
When odwiedzający zapisuje się adresem "ala@example.com"
Then zapis kończy się statusem "created"
And na liście jest 1 adres
Scenario: Niepoprawny adres e-mail
Given lista subskrybentów jest pusta
When odwiedzający zapisuje się adresem "ala.example.com"
Then zapis jest odrzucony z błędem
And na liście jest 0 adresów
Scenario: Ten sam adres po raz drugi
Given lista subskrybentów zawiera adres "ala@example.com"
When odwiedzający zapisuje się adresem "ala@example.com"
Then zapis kończy się statusem "duplicate"
And na liście jest 1 adres
Taki plik przeczyta i zatwierdzi osoba, która nie zna Pythona. Żeby scenariusze stały się testami, dopisujesz kroki (steps), czyli funkcje łączące zdania z kodem. Używamy biblioteki pytest-bdd (pip install pytest-bdd), plik test_newsletter_bdd.py:
import pytest
from pytest_bdd import given, parsers, scenarios, then, when
from newsletter import subscribe
scenarios("features/newsletter.feature")
@pytest.fixture
def subscribers():
return set()
@pytest.fixture
def outcome():
return {}
@given("lista subskrybentów jest pusta")
def empty_list(subscribers):
pass
@given(parsers.parse('lista subskrybentów zawiera adres "{email}"'))
def list_with_email(subscribers, email):
subscribers.add(email)
@when(parsers.parse('odwiedzający zapisuje się adresem "{email}"'))
def visitor_subscribes(subscribers, outcome, email):
try:
outcome["status"] = subscribe(email, subscribers)
except ValueError as error:
outcome["error"] = error
@then(parsers.parse('zapis kończy się statusem "{status}"'))
def status_is(outcome, status):
assert outcome["status"] == status
@then("zapis jest odrzucony z błędem")
def is_rejected(outcome):
assert "error" in outcome
@then(parsers.parse("na liście jest {count:d} adres"))
@then(parsers.parse("na liście jest {count:d} adresów"))
def list_size(subscribers, count):
assert len(subscribers) == count
Uruchomienie pytest -v:
test_newsletter.py::test_valid_email_is_saved PASSED
test_newsletter.py::test_invalid_email_is_rejected PASSED
test_newsletter.py::test_duplicate_email_is_not_saved_twice PASSED
test_newsletter_bdd.py::test_poprawny_adres_email PASSED
test_newsletter_bdd.py::test_niepoprawny_adres_email PASSED
test_newsletter_bdd.py::test_ten_sam_adres_po_raz_drugi PASSED
6 passed in 0.01s
Zwróć uwagę, że kroki wołają ten sam subscribe(), który powstał w TDD. Fixtures (subscribers, outcome) przekazują stan między krokami Given, When i Then, tak jak w zwykłych testach pytest. Więcej o fixtures i izolacji testów znajdziesz w artykule o testowaniu bazy danych w pytest i SQLAlchemy.
Przypnij wersje bibliotek. Przy przygotowaniu tego przykładu (31 października 2026) pytest-bdd 9.0.0 nie działał z pytest 9.2.0. Każdy scenariusz kończył się błędem z wnętrza biblioteki, a nie z mojego kodu:
AttributeError: 'FixtureManager' object has no attribute '_arg2fixturedefs'
Po zainstalowaniu pytest 9.0.3 wszystkie scenariusze przechodziły. Lekcja jest ogólna: wpisz wersje do requirements.txt (np. pytest==9.0.3, pytest-bdd==9.0.0), a gdy zobaczysz błąd z wnętrza biblioteki, najpierw sprawdź zgodność wersji, zanim zaczniesz szukać pomyłki we własnych krokach. Wtyczki pytest sięgają do wewnętrznych elementów pytest, więc zdarza się, że nowe wydanie jednego pakietu psuje drugi. Ten konkretny problem mógł zostać już naprawiony w nowszych wydaniach.
TDD czy BDD: co wybrać i czy trzeba stosować oba?
Nie musisz wybierać. Najlepiej działają jako dwie warstwy:
- Zewnętrzna pętla (BDD): scenariusz mówi, co ma działać z punktu widzenia użytkownika. Na początku jest czerwony.
- Wewnętrzna pętla (TDD): małymi cyklami red-green-refactor piszesz kod, który stopniowo ten scenariusz zazielenia.
W małym projekcie lub na początku nauki wystarczy samo TDD. BDD zaczyna się opłacać, gdy wymagania zapisuje ktoś inny niż programista (osoba od produktu, klient, tester) albo gdy chcesz, żeby lista scenariuszy służyła za żywą dokumentację.
Czy do BDD potrzebuję pytest-bdd i plików .feature?
Nie. Pliki .feature i pytest-bdd opłacają się, gdy scenariusze czyta i zatwierdza ktoś spoza zespołu programistów. Gdy pracujesz sam albo w małym zespole, możesz myśleć w BDD bez żadnego dodatkowego narzędzia: nazwij test w stylu test_given_empty_list_when_invalid_email_then_error i ułóż jego ciało na trzy części, z komentarzami # Given, # When, # Then. Zyskujesz sposób myślenia BDD (zachowanie widoczne dla użytkownika, a nie szczegóły funkcji) bez narzutu biblioteki. Do plików .feature przejdziesz wtedy, gdy pojawi się osoba nietechniczna, która ma scenariusze zatwierdzać.
Jak TDD i BDD łączą się ze spec-driven development i pracą z AI?
W spec-driven development specyfikacja zawiera kryteria akceptacji, a te prowadzą wprost do testów: kryterium „niepoprawny e-mail jest odrzucany” to scenariusz BDD, a ten rozpada się na testy TDD pojedynczych funkcji. Dzięki temu masz łańcuch od wymagania po kod: kryterium akceptacji → scenariusz Given / When / Then → test w pytest → implementacja.
To szczególnie przydaje się przy kodzie od agenta AI. Gdy testy napiszesz przed zleceniem zadania, agent dostaje jednoznaczną miarę sukcesu, a ty nie oceniasz kodu „na oko”, tylko sprawdzasz, czy testy są zielone. Pomaga to też ograniczać problemy opisane w artykułach o halucynacjach AI w kodzie i bezpieczeństwie kodu generowanego przez AI: zmyślona funkcja lub brak walidacji wychodzą na jaw, gdy test jest czerwony. Te same scenariusze ułatwiają code review, bo recenzent porównuje zmianę z uzgodnionym zachowaniem.
W podejściu DDD scenariusze BDD mają jeszcze jedną zaletę: zdania Given / When / Then zapisujesz słowami z dziedziny biznesowej, czyli tym wspólnym słownikiem, który opisuję w artykule o DDD na przykładzie w Pythonie.
Jakie błędy popełniają początkujący w TDD i BDD?
- Pisanie testów po kodzie. Wtedy nie widzisz czerwonego testu i nie wiesz, czy cokolwiek sprawdza. To nadal przydatne testy, ale to już nie TDD.
- Za duże kroki. Test, który wymaga naraz walidacji, zapisu i wysyłki e-maila, nie mówi, co zawiodło. Jeden test to jedno zachowanie.
- Testowanie implementacji zamiast zachowania. Test sprawdzający, jak funkcja działa w środku, psuje się przy każdym refaktorze. Sprawdzaj wynik i efekt, nie kroki pośrednie.
- Scenariusze Gherkin jako skrypt klikania. „Given otwarta strona, When kliknij przycisk 3…” to opis interfejsu, a nie zachowania. Dobry scenariusz mówi, co chce osiągnąć użytkownik, a nie jak to klika.
- Zlecenie AI napisania testów do własnego kodu. Agent, który pisze test do kodu, który przed chwilą wygenerował, często potwierdza własne założenia, także błędne. Testy (a przynajmniej scenariusze) powinny pochodzić od ciebie, a implementacja od agenta.
Podsumowanie
- TDD to cykl red-green-refactor: test, najprostszy kod, porządki.
- BDD opisuje zachowanie scenariuszami Given / When / Then, zrozumiałymi także dla osób nietechnicznych.
- TDD pilnuje poprawności kodu, BDD zgodności z potrzebą; w praktyce BDD tworzy zewnętrzną, a TDD wewnętrzną pętlę.
- Kryteria akceptacji ze specyfikacji naturalnie zamieniają się w scenariusze, a te w testy.
- Przy pracy z AI pisz testy przed zleceniem zadania, a nie po nim.
Zadanie na dziś: weź jedną małą funkcję ze swojego projektu (np. walidację hasła). Spisz trzy kryteria akceptacji, zamień pierwsze na test, zobacz go na czerwono i dopiero wtedy napisz kod. Jeśli uczysz się Pythona w ramach ścieżki Python, poproś mentora o przegląd swoich testów: sprawdzi, czy każdy opisuje jedno zachowanie.


