Module 4: The Complete TDD Workflow

Red-Green-Refactor with AI

Red-Green-Refactor with AI

Capsule overview

In classic TDD, you write the failing test, you write the minimal code to pass, you refactor. The cycle is yours from end to end. With Claude Code, the cycle changes: you write the test, Claude Code writes the implementation, and the refactor is collaborative. The bottleneck stops being "writing code" and becomes "the quality of the tests" and "your judgment to evaluate". This capsule shows you exactly how the adapted cycle works and what your new role as architect and evaluator is.


The classic TDD cycle: A recap

The three phases

Every TDD cycle has three well-defined phases:

┌─────────────────────────────────────────────────────────────────────┐
│                    RED-GREEN-REFACTOR CYCLE                          │
└─────────────────────────────────────────────────────────────────────┘

    ┌──────────┐         ┌──────────┐         ┌──────────┐
    │   RED    │ ──────► │  GREEN   │ ──────► │ REFACTOR │
    └──────────┘         └──────────┘         └──────────┘
         │                     │                     │
         │                     │                     │
         ▼                     ▼                     ▼
    Write a test         Write the MINIMAL     Improve the code
    that FAILS           code to make          WITHOUT breaking
                         it pass               green tests
         │                     │                     │
         │                     │                     │
         └─────────────────────┴─────────────────────┘
                              │
                              │  back to RED
                              │  (a new test)
                              └──────────────────────►

RED: Write a test that fails

You write a test that describes the behavior you want. There's no implementation yet — the test fails. Goal: define what the code must do before writing it.

# test_email_validator.py — RED
import pytest

def test_validates_correct_email():
    from validators import validate_email
    assert validate_email("user@example.com") is True

If you run pytest test_email_validator.py, it fails: ModuleNotFoundError or ImportError because validate_email doesn't exist.

GREEN: Write the minimal code to pass

You implement the minimum necessary for the test to pass. You don't worry about elegance, DRY, or edge cases — only about making the test pass.

# validators.py — GREEN
def validate_email(email: str) -> bool:
    return "@" in email and "." in email.split("@")[-1]

The test passes. Done.

REFACTOR: Improve without breaking

With the green test as a safety net, you refactor: improve names, extract functions, handle edge cases. If something breaks, the tests tell you.

# validators.py — REFACTOR
import re

def validate_email(email: str) -> bool:
    """Validate email format using RFC 5322 simplified pattern."""
    if not email or not isinstance(email, str):
        return False
    pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
    return bool(re.fullmatch(pattern, email))

You run the tests. They still pass. You can continue with more confidence.


Why the order matters: RED before GREEN

In TDD, the order is sacred. If you write the implementation first and then the test, it isn't TDD — it's test-after. The psychological difference is enormous:

  • Test first: The test is a contract. You define what you want without being biased by what "is easy to implement". The implementation is forced to fulfill the contract.
  • Test after: The test verifies what the implementation already does. If the implementation has a bug, it's easy for the test to reflect it (or not even detect it, because you write the test while looking at the code).

With Claude Code, this discipline is even more critical. If you give it existing code and ask "write tests for this", the tests will tend to validate the current behavior — including bugs. If you give it tests and ask "implement this", the implementation is forced to fulfill the specification. Always RED before GREEN.


How the cycle changes with Claude Code

What stays the same: RED

RED doesn't change. You write the failing test. Nobody else. It's your responsibility to define what the system must do. Claude Code can't guess your intent — it needs the test as a specification.

RED (unchanged):
┌─────────────────────────────────────┐
│  YOU write the test that fails      │
│  You define the expected behavior   │
│  The test IS the specification      │
└─────────────────────────────────────┘

What changes: GREEN

GREEN is done by Claude Code. You give it the test (and optionally context). Claude Code generates the implementation. The "implementation" time goes from 10-30 minutes to ~30 seconds.

GREEN (changed):
┌─────────────────────────────────────┐
│  YOU: "Implement validate_email     │
│        that passes these tests"     │
│  CLAUDE CODE: generates the code    │
│  Time: ~30 seconds                  │
└─────────────────────────────────────┘

What is collaborative: REFACTOR

REFACTOR is collaborative. Claude Code can propose refactorings (simplify, extract a function, improve names). You approve, modify or reject. The tests remain your safety net — before accepting any change, you verify that the tests pass.

REFACTOR (collaborative):
┌─────────────────────────────────────┐
│  Claude Code proposes improvements  │
│  You evaluate: is it worth it?      │
│  Green tests = safety               │
└─────────────────────────────────────┘

The bottleneck shifts

In classic TDD, the bottleneck is usually writing the implementation. With Claude Code, the bottleneck becomes:

  1. The quality of the tests: If your test is ambiguous, Claude Code can implement something that "passes" but isn't what you wanted.
  2. Your judgment: Did Claude Code fail because the implementation is bad? Or because your test specified the behavior poorly?
Classic TDD:
  Bottleneck: writing code (10-30 min per cycle)

TDD with Claude Code:
  Bottleneck: test quality + judgment to evaluate

The rhythm of agentic TDD

Classic TDD: time distribution

A typical cycle in manual TDD:

Classic TDD cycle (example: 17 min total):
├── RED:    Write the test          ~2 min   (12%)
├── GREEN:  Implement the code     ~10 min  (59%)  ← most of it
└── REFACTOR: Improve the code     ~5 min   (29%)

Most of the time is spent implementing. Writing the test is fast; writing code is slow.

Agentic TDD: a new distribution

With Claude Code as the implementer:

Agentic TDD cycle (example: 10 min total):
├── RED:    Write the test (more thought)  ~5 min   (50%)  ← more time here
├── GREEN:  Claude Code implements         ~30 sec   (5%)
├── EVALUATE: Does it pass? Is it right?  ~3 min   (30%)  ← a new skill
└── REFACTOR: Adjust, simplify            ~2 min   (20%)

The developer spends more time thinking about tests and evaluating than waiting for code. The implementation arrives in seconds; the judgment remains human.

Your role changes

Before (classic TDD)Now (agentic TDD)
Writing codeDesigning effective tests
Implementing featuresEvaluating whether the implementation is correct
Refactoring manuallyApproving/rejecting refactor proposals
CoderArchitect + Evaluator

You're not a spectator. You guide the process: you define what gets built (tests), you approve or reject what Claude Code produces, and you refine iteratively.

Anti-patterns to avoid in agentic TDD

Anti-patternWhat happensWhat to do instead
A giant test that validates the whole featureClaude Code gets overwhelmed, incomplete implementationSmall tests: one behavior per test
Asking for an implementation without testsIt isn't TDD; manual validationAlways RED first
Accepting code without running the testsSilent bugsAlways pytest before approving
Iterating without giving contextClaude Code repeats the same mistakeInclude the pytest output, the test file
Letting Claude Code write the testsThe tests reflect the implementation, not the requirementsYou write the tests; Claude implements
Accepting everything the AI generatesCode that passes but doesn't meet the quality barAlways evaluate; ask for a refactor if needed

Reminder: ✅ You write the tests. ❌ Don't ask for an implementation without tests. ⚠️ Don't accept anything without running pytest.


A complete example: one cycle end to end

Feature: an email validator

You're going to build validate_email(email: str) -> bool using TDD with Claude Code.

Step 1: RED — You write the tests

You create tests/test_validators.py:

# tests/test_validators.py
import pytest

def test_validates_correct_email():
    from validators import validate_email
    assert validate_email("user@example.com") is True
    assert validate_email("admin@company.co.uk") is True

def test_rejects_invalid_email():
    from validators import validate_email
    assert validate_email("invalid") is False
    assert validate_email("user@") is False
    assert validate_email("@domain.com") is False
    assert validate_email("") is False

You run: pytest tests/test_validators.py → FAILED (the module or function doesn't exist).

Step 2: A prompt to Claude Code

You open the test file in Claude Code and write something like:

Implement the validate_email function in validators.py so it passes these tests.
The function receives a string and returns True if it's a valid email, False otherwise.
Keep the interface: validate_email(email: str) -> bool

Step 3: GREEN — Claude Code implements

Claude Code generates validators.py:

# validators.py
import re

def validate_email(email: str) -> bool:
    """Validate email format."""
    if not email or not isinstance(email, str):
        return False
    pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
    return bool(re.fullmatch(pattern, email))

You run: pytest tests/test_validators.py → PASSED.

Step 4: Evaluate

This is the phase that didn't exist as such in classic TDD. When you implement, "evaluating" is usually automatic (you know it's right because you wrote it). With Claude Code, you must judge explicitly:

Evaluation checklist:

  1. Do the tests pass? Run pytest. If they fail, go back to Claude Code with the error output.
  2. Does it handle the edge cases you thought of? Check: empty strings, None, wrong types. If you didn't test them, consider adding a cycle.
  3. Is the implementation reasonable? Does it use standard patterns? Is it readable?
  4. Is there anything that bothers you? If something "looks odd" but passes the tests, it could be a latent bug. Add a test for that case.

In this example: the tests pass, the regex is standard, and the edge cases (empty, type) are covered. Approved.

Step 5: REFACTOR (if applicable)

You can ask Claude Code: "Simplify or improve the code while keeping the tests green." Or you can leave it as is if it's already good enough.

A real interaction with Claude Code (example prompt):

I have these tests in tests/test_validators.py that currently fail because 
validate_email doesn't exist. I need you to create the validators.py file with a 
validate_email(email: str) -> bool function that makes them pass.

Requirements:
- Accepts emails with the format user@domain.tld
- Rejects: empty, no @, no domain, no TLD

Claude Code responds with the code. You run pytest. If it passes, you move on to evaluating. If it fails, you copy the error message and ask: "This test fails with this error: [output]. Fix the implementation."


Multiple cycles for one feature

A real feature is rarely done in a single cycle. You build it in layers.

Cycle 1: basic validation (format)

Tests:

def test_validates_correct_email():
    assert validate_email("user@example.com") is True

def test_rejects_invalid_format():
    assert validate_email("invalid") is False
    assert validate_email("user@") is False

Implementation: Claude Code generates format validation (a basic regex). Green.

Cycle 2: domain validation

Additional tests:

def test_rejects_invalid_domain():
    assert validate_email("user@.com") is False
    assert validate_email("user@domain") is False  # no TLD

def test_accepts_common_tlds():
    assert validate_email("user@example.com") is True
    assert validate_email("user@example.org") is True

Implementation: Claude Code refines the regex or adds domain validation. Green.

Typical resulting code after cycle 2:

# validators.py — after cycle 2
import re

def validate_email(email: str) -> bool:
    if not email or not isinstance(email, str):
        return False
    # Ensures there's a domain and a TLD (at least 2 chars)
    pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
    if not re.fullmatch(pattern, email):
        return False
    # Rejects user@.com (empty domain) and user@domain (no TLD)
    local, domain = email.rsplit("@", 1)
    if not domain or "." not in domain or len(domain.split(".")[-1]) < 2:
        return False
    return True

Cycle 3: edge cases (unicode, length)

Additional tests:

def test_rejects_unicode_in_local_part():
    # Depending on policy: reject or accept
    assert validate_email("usér@example.com") is False  # a strict example

def test_rejects_email_too_long():
    long_email = "a" * 254 + "@b.com"  # > 254 chars
    assert validate_email(long_email) is False

Implementation: Claude Code adds length and character checks. Green.

Note: The unicode test is a policy decision. Some systems accept usér@example.com (RFC 6531). If your domain requires strict ASCII, the test makes sense. If not, you could remove that test or change it to is True. That's spec refinement in action.

A summary of the cycles

Cycle 1: Basic format      → test_validates_correct_email, test_rejects_invalid_format
Cycle 2: Domain            → test_rejects_invalid_domain, test_accepts_common_tlds
Cycle 3: Edge cases        → test_rejects_unicode, test_rejects_email_too_long

Each cycle builds on the previous one. The tests accumulate; the implementation evolves.


Your new role: architect, evaluator, refiner

Architect: you define WHAT the system does

You decide the behavior. The tests are your specification. You don't implement, but you do define what must happen. If you don't write a test for an edge case, Claude Code probably won't handle it.

Example: If you never write test_rejects_email_with_spaces, Claude Code could return True for "user @example.com". It's not that Claude Code is dumb — it's that it has no way of knowing that case matters. The test is the only way to communicate it.

Your responsibilities as architect:

  • ✅ Decide which success and failure cases to cover
  • ✅ Define the interface (function names, parameters, types)
  • ✅ Prioritize: which cycle goes first, which edge cases are critical
  • ❌ Don't implement — that's Claude Code's job

Evaluator: you judge whether it's correct AND good

When Claude Code generates code, your job is:

  1. Does it pass the tests? Objective. pytest tells you.
  2. Is it correct? Does it cover the cases you imagined? Is there logic that "works by accident"?
  3. Is it good? Readable? Maintainable? Clear names? Does it use standard libraries instead of reinventing the wheel?

Passing isn't enough. It must be code you want to maintain. If Claude Code implements a 500-character regex when email-validator exists, you can ask for a refactor: "Use the standard library or email-validator instead of a manual regex."

Your responsibilities as evaluator:

  • ✅ Run the tests and verify they pass
  • ✅ Review edge cases not covered by tests
  • ✅ Judge quality: readability, maintainability, dependencies
  • ❌ Don't accept code "because it comes from AI" — the bar is the same as for human code

Refiner: you improve tests and implementation iteratively

You discover that your spec was incomplete. You add tests. You ask Claude Code to adapt the implementation. Or the other way around: the implementation suggests the test was too strict — you adjust the test. Refinement is continuous.

An example of spec refinement: You wrote test_rejects_invalid_email assuming "user@.com" should be rejected. Claude Code implements it and it passes. But then you notice that "user@.com" is actually a valid domain in some contexts (e.g. .com as a TLD with an empty subdomain). You have two options: (1) adjust the test to accept that case, or (2) keep the test and document that your policy is to reject it. In both cases, you refined the spec based on what you learned during the cycle.

You are NOT a spectator

You don't just approve everything. You actively guide:

  • You write clear tests.
  • You give Claude Code enough context.
  • You decide when to iterate and when to step in.
  • You refine the spec when needed.

Practice: A minimal project structure

To replicate the cycle on your machine, use this structure:

my_project/
├── validators.py       # Claude Code will create/modify this file
├── tests/
│   └── test_validators.py   # You write the tests first
├── pyproject.toml      # or requirements.txt with pytest

Minimal setup:

# Create the structure
mkdir -p tests
touch validators.py tests/test_validators.py

# Install pytest if you don't have it
pip install pytest

Then: write the tests in test_validators.py, run pytest, you'll see RED. Pass the file and the prompt to Claude Code, and you'll get GREEN.

Complete code for the first cycle

File tests/test_validators.py (you write it — RED):

# tests/test_validators.py
"""Tests for validate_email. Run with: pytest tests/test_validators.py -v"""

def test_validates_correct_email():
    from validators import validate_email
    assert validate_email("user@example.com") is True
    assert validate_email("admin@company.co.uk") is True

def test_rejects_invalid_email():
    from validators import validate_email
    assert validate_email("invalid") is False
    assert validate_email("user@") is False
    assert validate_email("@domain.com") is False
    assert validate_email("") is False

File validators.py (Claude Code generates it — GREEN):

# validators.py
"""Email validator. Generated to pass tests/test_validators.py"""

import re

def validate_email(email: str) -> bool:
    """Validates email format. Returns True if valid, False otherwise."""
    if not email or not isinstance(email, str):
        return False
    pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
    return bool(re.fullmatch(pattern, email))

Commands to verify:

# 1. Without validators.py: RED (ModuleNotFoundError)
pytest tests/test_validators.py -v

# 2. With validators.py created by Claude Code: GREEN
pytest tests/test_validators.py -v
# You should see: 2 passed

Comparison: classic TDD vs agentic TDD

AspectClassic TDDAgentic TDD
Who writes the testYouYou
Who implementsYouClaude Code
Who refactorsYouCollaborative (Claude proposes, you approve)
RED time~2 min~5 min (more thought-out tests)
GREEN time~10-30 min~30 sec
REFACTOR time~5 min~2 min
BottleneckWriting codeTest quality + judgment
Key skillsCode, design, testsTest design, evaluation, judgment
Main roleCoderArchitect + evaluator

Conclusion: In agentic TDD, your value isn't in writing more code — it's in designing better specs (tests) and having the judgment to evaluate whether what Claude Code produces meets your standard. Code is a commodity; judgment is the differentiator.

Heuristics for when to intervene vs let it iterate

When Claude Code fails a test, you have to decide: do you give it another shot with more context, or do you step in?

SituationRecommended action
A clear failure: the test is explicit and the error obviousAsk Claude Code to fix it; include the pytest output
A repeated failure: Claude Code makes the same mistake 2+ timesYou step in or change the prompt's approach
The test might be incorrectReview the spec; adjust the test if appropriate
Several tests fail and there's a common patternGive more context: "The problem seems to be X. Prioritize passing tests A and B first."
The implementation is correct but messyAsk for a refactor keeping the tests green; evaluate the result

There's no universal rule. With practice you'll develop intuition. The key: don't assume the failure is always Claude Code's. Sometimes it's the test that's wrong.


Exercises

Exercise 1: Identify the phases (Easy)

Read this flow and label each step as RED, GREEN or REFACTOR:

1. You write test_parse_date_accepts_iso_format, which fails
2. Claude Code generates parse_date with datetime.strptime
3. Pytest passes
4. You ask Claude Code to extract the validation into a helper function
5. The tests still pass
See solution
  • Step 1: RED — You write a test that fails.
  • Step 2: GREEN — Claude Code implements the minimum to pass.
  • Step 3: GREEN — You verify that the tests pass.
  • Step 4: REFACTOR — You improve the structure without changing behavior.
  • Step 5: REFACTOR — You validate that the refactor didn't break anything.

Exercise 2: Write tests for cycle 1 (Medium)

You want an is_strong_password(password: str) -> bool function that returns True if the password has at least 8 characters, one uppercase letter and one number. Write 4 tests for the first cycle (RED) that define this behavior.

See solution
# tests/test_password_validator.py
import pytest

def test_accepts_valid_password():
    from validators import is_strong_password
    assert is_strong_password("SecurePass1") is True
    assert is_strong_password("MyP4ssw0rd") is True

def test_rejects_short_password():
    from validators import is_strong_password
    assert is_strong_password("Short1") is False

def test_rejects_without_uppercase():
    from validators import is_strong_password
    assert is_strong_password("alllowercase1") is False

def test_rejects_without_number():
    from validators import is_strong_password
    assert is_strong_password("NoNumbersHere") is False

These 4 tests define the minimal contract. Each one tests an independent criterion.

Exercise 3: A second cycle for the same feature (Medium)

For is_strong_password, cycle 1 covered length, uppercase and number. In cycle 2 you want to add: at least one special character (!@#$%^&*). Write 2 new tests for that cycle.

See solution
def test_accepts_password_with_special_char():
    from validators import is_strong_password
    assert is_strong_password("SecureP4ss!") is True
    assert is_strong_password("Pass@word1") is True

def test_rejects_without_special_char():
    from validators import is_strong_password
    assert is_strong_password("SecurePass1") is False  # now a special char is required

Note: If in cycle 1 SecurePass1 was valid, in cycle 2 the spec changes: now it must have a special character. The test_accepts_valid_password test from cycle 1 may need adjusting — or you could decide that the spec evolves and the previous tests get updated. That's spec refinement.

Exercise 4: Compare times (Easy)

In classic TDD, a cycle for a simple function takes ~15 min (2 RED + 10 GREEN + 3 REFACTOR). In agentic TDD, it takes ~8 min (4 RED + 0.5 GREEN + 2 EVALUATE + 1.5 REFACTOR). How many cycles do you complete in 1 hour with each approach? What does that imply for development speed?

See solution

Classic TDD: 60 min / 15 min ≈ 4 cycles/hour

Agentic TDD: 60 min / 8 min ≈ 7.5 cycles/hour

With agentic TDD you do almost twice as many cycles in the same time. The gain isn't just speed: more cycles mean more frequent feedback and less accumulated code before validating.

Exercise 5: When to intervene (Hard)

Claude Code implements validate_email and your test_rejects_empty_string test fails: the function returns True for "". When do you step in yourself vs when do you ask Claude Code to fix it? Write a decision criterion in 3-4 points.

See solution

You step in when:

  • The fix is trivial (1-2 lines) and you know exactly what to change
  • You want to practice and understand the code
  • It's a learning project and you want to touch the code

You ask Claude Code to fix it when:

  • The fix isn't obvious or requires more context than you already have
  • You prefer to keep the "test → Claude implements" flow
  • There are several failures and you want Claude Code to resolve them in one go

General criterion: If the test is clear and the failure is "the implementation doesn't meet the spec", Claude Code can usually fix it. If the failure indicates the test was ambiguous or incorrect, adjust the test yourself first.

Exercise 6: An effective prompt (Medium)

You have these tests written for format_currency(amount: float, currency: str) -> str:

def test_formats_usd():
    assert format_currency(99.99, "USD") == "$99.99"

def test_formats_eur():
    assert format_currency(99.99, "EUR") == "99,99 €"

Write a prompt you'd give Claude Code to implement the function. Include: the test file, the module's location, and any constraints (for example, a fixed locale).

See solution
Implement the format_currency(amount: float, currency: str) -> str function in the formatters.py module.

It must pass all the tests in tests/test_formatters.py.

Expected behavior:
- USD: $ prefix, period as the decimal separator (e.g. "$99.99")
- EUR: € suffix, comma as the decimal separator (e.g. "99,99 €")

Use Python's standard module for formatting (locale or decimal). 
If you use locale, configure "en_US" for USD and "de_DE" or similar for EUR to stay consistent with the tests.

A good prompt includes: the code's location, a reference to the tests, and concrete output examples.


Troubleshooting

Problem 1: Claude Code implements something that passes the test but isn't what you wanted

Symptom: The tests pass, but the behavior in real cases is incorrect.

Cause: The test was too weak or ambiguous. For example, you only tested a happy path.

Example: You asked for validate_email and only tested "user@example.com". Claude Code implemented return "@" in email — technically it passes, but it accepts "@" or "a@b" as valid.

Solution: Strengthen the test. Add cases that capture the behavior you expected: assert validate_email("a@b") is False, assert validate_email("@") is False. Run it again. If the new test fails, ask Claude Code to update the implementation so it also passes that test.

Problem 2: Claude Code generates code that fails multiple tests at once

Symptom: After generating the implementation, several tests fail.

Cause: The prompt was unclear, there were too many requirements at once, or the tests have hidden dependencies.

Solution:

  • Reduce the scope: ask it to implement for just 1-2 tests first.
  • Give it the complete test file and pytest's error message.
  • If one test depends on another (for example, fixtures), make that clear in the prompt.

Problem 3: You don't know if the problem is the test or the implementation

Symptom: A test fails and it isn't clear whether the test is wrong or the implementation is.

Cause: An incomplete spec or ambiguity in the requirements.

Solution:

  • Review the original requirement. Does the test reflect it well?
  • Ask: "If a human implemented this correctly, would it pass my test?" If the answer is no, fix the test.
  • When in doubt, ask Claude Code: "This test fails. Is the expected behavior in the test correct according to the typical requirements of [domain]?"

Problem 4: The cycles get too long

Symptom: Each cycle takes 20+ minutes because the tests are enormous or the prompt is too broad.

Cause: Tests that validate too much at once, or unfocused prompts.

Solution:

  • One test, one purpose. Split it into smaller tests.
  • Cycle 1: only the most basic case. Cycle 2: one edge case. Cycle 3: another.
  • In the prompt, state explicitly: "Implement only enough to pass these 2 tests for now."

Problem 5: Claude Code refactors and breaks tests

Symptom: After a refactor proposed by Claude Code, some test fails.

Cause: The refactor changed behavior unintentionally, or the test depended on implementation details.

Solution:

  • Don't accept refactors without running the tests beforehand.
  • If it fails: revert the refactor or ask Claude Code to adjust it while keeping the tests green.
  • If the test was failing because it depended on the implementation (for example, the order of internal calls), adjust the test so it validates behavior, not implementation.

Quick troubleshooting summary:

SymptomProbable causeAction
The test passes but the behavior is wrongA weak testStrengthen the tests, add cases
Several tests fail at onceA broad prompt or complex testsReduce the scope, 1-2 tests per cycle
You don't know if the error is the test or the implementationAn ambiguous specReview the requirements, get a second opinion
Very long cyclesGiant testsSplit them: one test, one purpose
A refactor breaks testsA behavior change or a fragile testRevert or adjust the test
Claude Code repeats the same mistakeLack of context or an ambiguous testStep in manually or rewrite the test

Project Connection

This module's project is to build an authentication system with TDD. The red-green-refactor cycle with AI that you practiced here is exactly the one you'll use:

Cycle 1: test_register_validates_email     → Claude implements email validation
Cycle 2: test_register_hashes_password     → Claude implements hashing
Cycle 3: test_register_creates_user         → Claude implements creation in the DB
Cycle 4: test_login_returns_token           → Claude implements login
Cycle 5: test_token_validates_correctly     → Claude implements JWT validation
...

Each feature is built with multiple small cycles. Your role will be to design clear tests, give Claude Code context, and evaluate each implementation before moving on. The project will let you practice the real rhythm: cycles of minutes, spec refinement when you discover missing requirements, and the judgment of when to step in.


Summary

The essentials of the adapted cycle:

  • ✅ RED doesn't change: you write the failing test. It's your specification.
  • ✅ GREEN is done by Claude Code: an implementation in ~30 seconds.
  • ✅ REFACTOR is collaborative: Claude Code proposes, you approve. The tests are your safety net.
  • ✅ The bottleneck shifts from "writing code" to "test quality" and "judgment to evaluate".
  • ✅ Your role: architect (you define what), evaluator (you judge whether it's correct and good), refiner (you improve iteratively). Not a spectator.
  • ✅ Multiple cycles per feature: each cycle adds a layer (format → domain → edge cases).
  • ✅ Small tests, one purpose per test. Cycles of minutes, not hours.
  • ✅ Anti-patterns: avoid giant tests, implementing without tests, accepting without running pytest.

Next capsule: Writing Failing Tests First — how to write effective red tests that give Claude Code the information it needs to implement correctly.


Additional Resources

  1. Kent Beck: Test-Driven Development by Example — The book that popularized red-green-refactor
  2. Martin Fowler: Refactoring — Refactoring techniques with tests as a safety net
  3. Tweag: Spec-Driven Development with LLMs — Spec-first applied to development with LLMs
  4. pytest: Writing and Running Tests — A reference for test structure
  5. The Pragmatic Programmer: Red-Green-Refactor — Context for the TDD cycle
  6. Anthropic: Claude Code Best Practices — How to give Claude Code effective context

Module 4, Capsule 02 — Testing with Claude Code Guide