Przejdź do głównej zawartości

TDD z agentami AI: najpierw czerwony test

Programowanie sterowane testami (TDD) z agentami AI oznacza, że zatwierdzony przez człowieka, nieprzechodzący test istnieje i pada z właściwego powodu, zanim agent napisze jakąkolwiek implementację. Agent zmienia potem kod aplikacji, aż zestaw testów przejdzie, przy zablokowanych plikach testów. Tak samo naprawia się błędy: odtwórz defekt czerwonym testem, zacommituj go, a potem napraw kod.

Ta strona jest dla programistów, którzy na co dzień prowadzą Claude Code, Codeksa albo Cursora, i dla tech leadów ustalających pętlę dla całego zespołu. Prosisz agenta o helper do kuponów. Dostajesz kod i zestaw testów, który przechodzi, a dwa dni później dział finansów zgłasza, że wygasłe kupony wciąż dają rabat. Testy powstały po kodzie, więc dowiodły tylko tego, że kod robi to, co robi kod.

  • Czterofazową pętlę (czerwony, potwierdzenie czerwonego, zielony, refaktoryzacja), w której każda faza to osobny, wąski prompt
  • Gotowe do wklejenia prompty na każdą fazę oraz szablon zamieniający listę wymagań w specyfikację testów
  • Protokół naprawy błędów od testu: test reprodukujący, commit czerwonego stanu, zablokowane testy, zweryfikowany zielony przebieg
  • Skrypt, który dowodzi, że czerwony test padał przed poprawką i że żaden test nie zmienił się potem
  • Objawy tego, że agent osłabia testy, by wymusić zielony wynik, i sposób wyjścia z tej sytuacji

Dlaczego test musi powstać pierwszy, gdy kod pisze agent?

Dział zatytułowany „Dlaczego test musi powstać pierwszy, gdy kod pisze agent?”

Agent optymalizuje pod cel, który mu dasz. Powiedz „zaimplementuj funkcję”, a „gotowe” oznacza to, co agent uzna za gotowe. Daj mu nieprzechodzący test, a „gotowe” staje się sprawdzeniem, które agent może uruchomić i z którym nie może dyskutować. To sedno weryfikowania zachowania zamiast czytania każdego diffa: przeglądasz test, który jest krótki i wyraża intencję, a o implementacji rozstrzyga zestaw testów.

Zespół DORA z Google Cloud napisał to samo o dostarczaniu wspieranym przez AI w raporcie z 2025 roku (2025-09-23): „Without robust control systems, like strong automated testing, mature version control practices, and fast feedback loops, an increase in change volume leads to instability”. Nieprzechodzący test, który istnieje przed kodem, to najmniejsza jednostka takiego systemu kontroli.

Kolejność ma znaczenie jeszcze z jednego powodu. Gdy ta sama tura pisze i test, i kod, agent zwykle pisze test potwierdzający to, co robi jego implementacja, łącznie z błędami. Rozdzielenie tur i zatwierdzenie testów przez człowieka pomiędzy nimi sprawia, że zielony przebieg w ogóle coś znaczy.

Klasyczny cykl to czerwony, zielony, refaktoryzacja. Z agentem każda faza staje się osobną instrukcją, a całość trzyma jedna reguła: tura, która pisze nieprzechodzący test, nigdy nie pisze kodu, który go spełnia.

  1. Napisz testy (czerwony). Nazwij zachowania i przypadki brzegowe, zabroń implementacji. Opisujesz kontrakt, a nie zamawiasz funkcję. Zacznij od trzech do pięciu testów podstawowego zachowania; przypadki brzegowe dodaj, gdy pierwszy wycinek będzie zielony.
  2. Potwierdź czerwony. Agent uruchamia zestaw i pokazuje niepowodzenia. Każde musi dotyczyć brakującego zachowania, a nie literówki w imporcie czy zepsutej fixtury. Tu zatwierdzasz testy: to twój przegląd specyfikacji.
  3. Zacommituj czerwony stan. Zacommituj same pliki testów (test: …). Commit jest dowodem, że test istniał i padał przed jakąkolwiek poprawką, oraz punktem, do którego wracasz, jeśli faza zielona pójdzie źle.
  4. Implementuj do zielonego. Jedna instrukcja: spraw, by te testy przeszły, nie ruszaj plików testów. Agent uruchamia zestaw po każdej zmianie i iteruje, aż wszystko będzie zielone, łącznie z resztą zestawu.
  5. Refaktoryzuj przy zielonym. Poproś o poprawę czytelności, która zmienia tylko implementację. Zablokowane testy są teraz siatką bezpieczeństwa, dzięki której przebudowa jest tania.

Zacznij od zachowania, nie od implementacji. Konkretne wymagania dają konkretne testy; ogólnikowy prompt daje testy sprawdzające, że funkcja istnieje. Użyj tego szablonu, gdy zaczynasz od dokumentu z wymaganiami i chcesz, żeby agent wyliczył przypadki:

Podmień FEATURE, REQUIREMENTS, TEST_FILE_PATH i EXISTING_TEST_FILE. Oto ten sam szablon wypełniony dla rate limitera w Expressie, gdzie współbieżność to dokładnie ten przypadek, którego agent sam z siebie nie przetestuje:

Czysta funkcja daje schludne demo i niewiele uczy. Oto pętla na przypadku z produkcji: metoda serwisu ze ścieżkami błędów i zamockowanym repozytorium.

Faza 1: tylko testy. Przypnij agenta do pisania testów i niczego więcej.

Faza 2: potwierdź czerwony. Nie pomijaj tego kroku. Test, który przechodzi już teraz, niczego nie sprawdza.

Przeczytaj listę niepowodzeń, zatwierdź testy i je zacommituj: git add src/services/pricing.test.ts && git commit -m "test: specify PricingService.applyCoupon".

Faza 3: implementacja do zielonego. Dopiero teraz pozwalasz na implementację i odgradzasz testy.

Pierwsza reguła jest kluczowa. Bez niej agent, który utknie, często „naprawia” asercję zamiast kodu. Limit prób wynika z tej samej logiki co w agentach programistycznych Stripe’a, które, jak napisał Alistair Gray ze Stripe’a 2026-02-09, dostają „at most two rounds of CI”, po czym, jak podaje wpis uzupełniający z 2026-02-19, gałąź wraca do jej ludzkiego operatora: ograniczona pętla pada głośno, zamiast dryfować.

Faza 4: refaktoryzacja przy zielonym.

Naprawiaj błędy od testu: odtwórz, zacommituj, zablokuj, napraw

Dział zatytułowany „Naprawiaj błędy od testu: odtwórz, zacommituj, zablokuj, napraw”

Naprawa błędu to ta sama pętla, tyle że test pisze się pod defekt, a nie pod funkcję. Gdy wklejasz stack trace i piszesz „napraw to”, agent często zmienia kod, aż objaw zniknie, i ogłasza sukces, a nic nie dowodzi, że defekt istniał ani że zniknął. Protokół „najpierw test” wymusza ten dowód. To także odpowiedź, za którą Developer Scorecard daje maksimum punktów w pytaniu o naprawę błędów: zacommitowany nieprzechodzący test, blokada plików testów na czas poprawki i zweryfikowany zielony przebieg.

  1. Odtwórz błąd nieprzechodzącym testem. Daj agentowi zgłoszenie albo log i zabroń zmian w kodzie źródłowym.

  2. Zacommituj czerwony test osobno. Zacznij komunikat od test: reproduce i zapisz SHA (git rev-parse HEAD): podasz je skryptowi weryfikującemu poniżej.

    Okno terminala
    # Terminal, katalog główny repozytorium
    git add tests/auth/email-change.test.ts
    git commit -m "test: reproduce session invalidation on email change"
  3. Zablokuj pliki testów na czas poprawki. Użyj blokady dla swojego narzędzia z następnej sekcji. Sama instrukcja w prompcie to prośba, a nie zabezpieczenie.

  4. Poproś o poprawkę.

  5. Zweryfikuj zielony przebieg skryptem poniżej, a nie czytaniem diffa: reprodukcja padała przed poprawką, przechodzi po niej, cały zestaw jest zielony, a żaden plik testu nie zmienił się po czerwonym commicie.

Pull request z poprawką zbudowany w ten sposób ma co najmniej dwa commity: czerwony test, a potem poprawkę. Test reprodukujący zostaje w zestawie na stałe jako test regresji.

Jak zablokować pliki testów, gdy agent implementuje?

Dział zatytułowany „Jak zablokować pliki testów, gdy agent implementuje?”

W każdym prompcie fazy zielonej pisz „do not modify the test files” i podeprzyj to mechanizmem, którego agent nie obejdzie argumentacją. Mechanizmy różnią się między narzędziami; pełną, wielowarstwową konfigurację, łącznie z CODEOWNERS i sprawdzeniem w CI, którego pull request nie może zmienić, znajdziesz na stronie o ochronie wyroczni.

Dodaj reguły deny do .claude/settings.json. Reguły deny blokują w każdym trybie uprawnień, a reguła Edit(...) obejmuje też Write:

{
"permissions": {
"deny": ["Edit(tests/**)", "Edit(**/*.test.ts)", "Edit(**/__snapshots__/**)"]
}
}

Reguły deny nie powstrzymają skryptu uruchomionego przez agenta przed zapisem plików. Do tego potrzebujesz filesystem.denyWrite w sandboksie i hooka PreToolUse, który mówi Claude’owi, co zrobić zamiast edycji testu; oba są na stronie o wyroczni. Hook blokuje tylko wtedy, gdy kończy się kodem 2; kod 1 przepuszcza edycję.

Skąd wiesz, że zielony wynik jest prawdziwy, bez czytania kodu?

Dział zatytułowany „Skąd wiesz, że zielony wynik jest prawdziwy, bez czytania kodu?”

Nie musisz czytać implementacji linijka po linijce, żeby jej zaufać. Potrzebujesz czterech faktów, a skrypt ustali każdy z nich:

  1. Test reprodukujący albo specyfikujący padał na czerwonym commicie.
  2. Przechodzi na HEAD.
  3. Cały zestaw przechodzi na HEAD.
  4. Żaden plik testu nie zmienił się między czerwonym commitem a HEAD.
#!/usr/bin/env bash
# scripts/verify-tdd.sh: uruchamiany w terminalu albo w CI na gałęzi z poprawką lub TDD
set -euo pipefail
TEST_FILE=${1:?usage: verify-tdd.sh <test file> <red commit sha>}
RED=$(git rev-parse --verify "${2:?usage: verify-tdd.sh <test file> <red commit sha>}^{commit}")
git merge-base --is-ancestor "$RED" HEAD || { echo "FAIL: $RED is not an ancestor of HEAD"; exit 1; }
git cat-file -e "$RED:$TEST_FILE" || { echo "FAIL: $TEST_FILE does not exist at $RED"; exit 1; }
ORIG=$(git symbolic-ref -q --short HEAD || git rev-parse HEAD)
changed=$(git diff --name-only "$RED" HEAD -- 'tests/' '*.test.ts' '*.spec.ts' \
'**/__snapshots__/**' 'vitest.config.*' 'jest.config.*')
if [ -n "$changed" ]; then
echo "FAIL: test files or test config changed after the red commit:"; echo "$changed"; exit 1
fi
trap 'git checkout --quiet "$ORIG"' EXIT
git checkout --quiet --detach "$RED"
if npx vitest run "$TEST_FILE" >/dev/null 2>&1; then
echo "FAIL: $TEST_FILE passes without the fix, so it does not reproduce anything"; exit 1
fi
git checkout --quiet "$ORIG"
npx vitest run "$TEST_FILE"
npx vitest run
echo "PASS: red before the fix, green after it, tests untouched"

Czerwony commit podajesz jawnie: scripts/verify-tdd.sh src/services/pricing.test.ts <red-sha>, czyli SHA zatwierdzone przez człowieka przy potwierdzaniu fazy czerwonej. Skrypt celowo nie szuka go w opisach commitów: gdyby brał ostatni commit test: , późniejszy commit osłabiający test pod tym samym prefiksem stałby się nowym punktem odniesienia, a sprawdzenie „żaden plik testów się nie zmienił” przeszłoby właśnie przy zmianie, którą ma wyłapywać. Skrypt wymaga czystego drzewa roboczego (najpierw commit albo stash). Kończy się błędem, jeśli czerwony commit nie jest przodkiem HEAD, więc SHA z innej gałęzi, na której test akurat pada z niezwiązanych powodów, nie może udawać punktu odniesienia, a sprawdzenie różnic zawsze porównuje historię tej samej gałęzi. Kończy się błędem, jeśli pliku testu nie było na czerwonym commicie, bo Vitest zwraca niezerowy kod także wtedy, gdy nie znajdzie żadnego pliku testu, więc literówka liczyłaby się jako „czerwony”. Sprawdzenie „żaden plik testów się nie zmienił” obejmuje też snapshoty oraz vitest.config.* / jest.config.*, bo wykluczenie testu w konfiguracji albo przepisanie snapshotu osłabia zestaw tak samo jak edycja testu. trap przywraca twoją gałąź albo odłączony commit, który pobrało CI, nawet gdy któryś krok się nie powiedzie. Wzorce ścieżek w git diff, takie jak '*.test.ts', pasują na dowolnej głębokości. Dostosuj ścieżki i runner do swojego stosu: pytest i go test działają tak samo, bo sprawdzenie opiera się wyłącznie na kodzie wyjścia.

Kto co zatwierdza:

ArtefaktKto zatwierdzaDowód
Lista testów i asercjeCzłowiek, na kroku potwierdzenia czerwonegoWynik niepowodzeń i czerwony commit
ImplementacjaZestaw testów, nie czytelnikverify-tdd.sh przechodzący w CI
Każda zmiana testu po czerwonym commicieCzłowiek, w osobnym pull requeścieWłaściciel plików testów w CODEOWNERS

Zielony zestaw dowodzi tylko tego, co sprawdzają testy. Żeby się dowiedzieć, czy złapałyby prawdziwy błąd, zmierz siłę wyroczni testami mutacyjnymi na zmienionym kodzie i dodaj testy oparte na właściwościach tam, gdzie przestrzeni wejść nie da się wyliczyć ręcznie.

Fazy są identyczne w każdym narzędziu. Różni się to, jak agent uruchamia zestaw i jaką część pętli test–poprawka–test wykonuje bez nadzoru.

W sesji interaktywnej wklejaj prompty kolejnych faz. Wpisz dokładne polecenia testów do CLAUDE.md (jeden plik, testy powiązane, cały zestaw), żeby Claude nie zgadywał między npm test a npx vitest. Żeby „gotowe” znaczyło „zielone”, dodaj hook Stop, który uruchamia zestaw i kończy się kodem 2, dopóki testy padają; przetestowaną wersję znajdziesz w przewodniku po hookach.

Skryptową fazę zieloną uruchom w trybie headless, tylko z potrzebnymi narzędziami:

Okno terminala
# Terminal albo CI (Claude Code 2.1.283)
claude -p "Implement src/services/pricing.ts so every test in src/services/pricing.test.ts passes. Run 'npx vitest run' and iterate until green. Do not edit any *.test.ts file." \
--permission-mode acceptEdits \
--allowedTools "Read,Edit,Write,Bash(npx vitest *)"

Jeśli Claude krąży wokół tego samego niepowodzenia, użyj /clear i zacznij nową sesję od promptu, który podaje jeden padający test i błąd, a nie całe zadanie.

O wyborze modelu: zacznij od domyślnego modelu narzędzia (Claude Opus 5.5 w Claude Code od v2.1.280 na kanale latest, GPT-6 Astra w Codeksie) i zwiększ effort, zanim zmienisz model; w Claude Code pierwszą dźwignią przy trudnej fazie zielonej jest /effort high. Claude Fable 5.1 (/model fable) wybieraj świadomie do najdłuższych i najtrudniejszych implementacji. Ceny i kompromisy znajdziesz w przeglądzie modeli.

Użyj skilla TDD zamiast wklejać reguły za każdym razem

Dział zatytułowany „Użyj skilla TDD zamiast wklejać reguły za każdym razem”

Jeśli prowadzisz tę pętlę codziennie, zainstaluj ją jako skill, żeby agent trzymał się jej bez długiego promptu. Dwa są szeroko instalowane, mają łącznie około 967 000 i 237 000 instalacji w skills.sh według zestawienia LinklyAI/best-skills z 2026-09-26 (źródło wtórne): tdd Matta Pococka (red-green-refactor po jednym pionowym wycinku, testowanie na uzgodnionych „szwach”) i skill test-driven-development z Superpowers (ścisłe red-green-refactor, zapisane jako „Iron Law”).

Okno terminala
# Terminal, katalog główny repozytorium: jedno polecenie instaluje skill dla trzech narzędzi
npx skills add mattpocock/skills --skill tdd -a claude-code -a codex -a cursor
npx skills add obra/superpowers --skill test-driven-development -a claude-code -a codex -a cursor

Skill zmienia to, co agent próbuje zrobić; niczego nie blokuje. Zachowaj reguły deny, profil albo hook oraz skrypt weryfikujący. Porównanie tych skilli i instalację jako pluginy znajdziesz na stronie skille, które uczą agenta porządnie testować i debugować.

  • Testy przechodzą, zanim kod istnieje. Zamockowana zależność domyślnie zwraca wartość truthy albo import trafia w starą zaślepkę. Co zrobić: nigdy nie pomijaj potwierdzenia czerwonego; jeśli wynik jest zielony, usuń test i napisz go od nowa pod publiczny interfejs.
  • Agent osłabia test, żeby wymusić zielony wynik. Rzadko wygląda to dramatycznie: expect(res.status).toBe(429) zmienia się w toBeDefined(), toBe(expected) w toBeTruthy(), „niestabilny” test znika albo mock zastępuje dokładnie to zachowanie, które test miał sprawdzać. Co zrobić: git checkout <red-commit> -- tests/, żeby przywrócić testy, dodaj blokadę dla swojego narzędzia i uruchom prompt fazy zielonej ponownie. Zestaw, który się skurczył, to sygnał alarmowy.
  • Test i poprawka w tej samej turze. Agent pisze test, który potwierdza jego własne błędne zachowanie. Co zrobić: wyrzuć jedno i drugie i zacznij od fazy czerwonej w nowej sesji.
  • Testy są trywialne. „Write tests for this function” daje testy sprawdzające, że funkcja istnieje. Co zrobić: nazwij każde zachowanie i podaj w prompcie konkretne pary wejście–wyjście.
  • Testy są sklejone z implementacją. Sprawdzają prywatne metody albo wewnętrzne struktury danych, więc każda refaktoryzacja je psuje. Co zrobić: przepisz je pod publiczne API; skill tdd Pococka kieruje agenta w tę stronę, testując na uzgodnionych „szwach”.
  • Niestabilne testy asynchroniczne „naprawiane” przez sleep. Co zrobić: poproś o deterministyczną kontrolę („use vi.useFakeTimers() and advance time explicitly; do not add real delays”).
  • Zestaw jest zbyt wolny, żeby na nim iterować. Agent czeka minutami na każdą próbę i zużywa kontekst. Co zrobić: w pętli uruchamiaj tylko testowany plik (npx vitest run src/services/pricing.test.ts; Jest od wersji 30 używa --testPathPatterns), a cały zestaw raz na końcu.
  • Poprawka psuje sąsiednie testy. Reprodukcja robi się zielona, a trzy inne testy czerwone. Co zrobić: prompt fazy zielonej wymaga przebiegu całego zestawu, a verify-tdd.sh blokuje gałąź, dopóki zestaw nie przejdzie.
  • Trzydzieści testów przed pierwszą linijką kodu. To zwykły paraliż analityczny, tylko w przebraniu procesu. Co zrobić: trzy do pięciu testów podstawowego zachowania, implementacja, a potem przypadki brzegowe w osobnym przebiegu, w którym testy mają na początku padać.