Module 4: The Complete TDD Workflow

Module Project: A Complete Feature with TDD

Module Project: A Complete Feature with TDD

Project overview

You've learned the complete TDD cycle with Claude Code: writing red tests, letting Claude implement, evaluating, refactoring. Now you're going to experience the real workflow by building a complete feature from start to finish: an authentication system with registration, login, and token validation.

Every aspect of the system starts as a failing test. Claude Code implements. You evaluate. You refactor. You move on to the next test. By the end, you'll have a working system built cycle by cycle — and you'll have experienced spec refinement, Claude Code's mistakes, and the judgment of when to step in.

The key isn't the final code — it's the process. You're going to document each TDD cycle to internalize the rhythm.


Project Objective

Build a complete authentication system using TDD with Claude Code, experiencing multiple consecutive red-green-refactor cycles.

By completing this project:

  • ✅ You'll have run 8+ complete TDD cycles
  • ✅ You'll have experienced spec refinement (adjusting tests when you discover gaps)
  • ✅ You'll have handled situations where Claude Code fails and needs more context
  • ✅ You'll have refactored code with tests as a safety net
  • ✅ You'll have a working authentication system built test-first

Technical Specifications

Technology Stack

  • Language: Python 3.10+
  • Testing: pytest
  • Hashing: hashlib (stdlib) — no external dependencies, to keep it simple
  • Tokens: secrets + json (a simple JWT simulation without dependencies)
  • AI: Claude Code

Initial Setup

mkdir auth-tdd-project
cd auth-tdd-project
python -m venv venv
source venv/bin/activate
pip install pytest

Project Structure

auth-tdd-project/
├── auth/
│   ├── __init__.py
│   ├── validators.py     ← Email and password validation (you create it with TDD)
│   ├── hasher.py          ← Password hashing (you create it with TDD)
│   ├── token_manager.py   ← Token generation and validation (you create it with TDD)
│   └── auth_service.py    ← Authentication service (you create it with TDD)
├── tests/
│   ├── __init__.py
│   ├── test_validators.py
│   ├── test_hasher.py
│   ├── test_token_manager.py
│   └── test_auth_service.py
└── requirements.txt

The TDD Cycles

Phase 1: Validators (Cycles 1-3)

Cycle 1: Email validation

# tests/test_validators.py — Write it FIRST

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

def test_email_without_at_fails():
    assert validate_email("userexample.com") is False

def test_email_without_domain_fails():
    assert validate_email("user@") is False

def test_empty_email_fails():
    assert validate_email("") is False

Your turn:

  1. Write these tests
  2. Run pytest tests/test_validators.py -v — all red
  3. Prompt to Claude Code: "Implement validate_email in auth/validators.py so these tests pass."
  4. Evaluate the implementation
  5. Refactor if necessary

Cycle 2: Password validation

def test_password_too_short_fails():
    assert validate_password("Ab1!") is False

def test_password_without_uppercase_fails():
    assert validate_password("abcdef1!") is False

def test_password_without_number_fails():
    assert validate_password("Abcdef!!") is False

def test_valid_password_passes():
    assert validate_password("Abcdef1!") is True

Cycle 3: Specific error messages

def test_validate_password_returns_errors():
    result = validate_password_detailed("abc")
    assert not result["is_valid"]
    assert "At least 8 characters" in result["errors"]
    assert "At least one uppercase" in result["errors"]
    assert "At least one number" in result["errors"]
    assert "At least one special character" in result["errors"]

Spec refinement: In Cycle 2, validate_password returns a bool. Now in Cycle 3, you need a different function that returns details. This is spec refinement — you discover during the process that you need more granularity.

Phase 2: Password Hashing (Cycles 4-5)

Cycle 4: Basic hashing

# tests/test_hasher.py

def test_hash_password_returns_string():
    hashed = hash_password("MyPassword1!")
    assert isinstance(hashed, str)
    assert hashed != "MyPassword1!"

def test_hash_password_is_deterministic_with_same_salt():
    hashed1 = hash_password("MyPassword1!", salt="fixed-salt")
    hashed2 = hash_password("MyPassword1!", salt="fixed-salt")
    assert hashed1 == hashed2

def test_different_passwords_produce_different_hashes():
    hash1 = hash_password("Password1!")
    hash2 = hash_password("Password2!")
    assert hash1 != hash2

Cycle 5: Password verification

def test_verify_password_correct():
    hashed = hash_password("MyPassword1!")
    assert verify_password("MyPassword1!", hashed) is True

def test_verify_password_incorrect():
    hashed = hash_password("MyPassword1!")
    assert verify_password("WrongPassword", hashed) is False

Phase 3: Token Manager (Cycles 6-7)

Cycle 6: Token generation

# tests/test_token_manager.py
import json

def test_generate_token_returns_string():
    token = generate_token(user_id=1, email="user@test.com")
    assert isinstance(token, str)
    assert len(token) > 0

def test_generate_token_contains_user_data():
    token = generate_token(user_id=42, email="test@example.com")
    decoded = decode_token(token)
    assert decoded["user_id"] == 42
    assert decoded["email"] == "test@example.com"

def test_generate_token_has_expiration():
    token = generate_token(user_id=1, email="test@test.com")
    decoded = decode_token(token)
    assert "exp" in decoded

Cycle 7: Token validation

def test_validate_valid_token():
    token = generate_token(user_id=1, email="user@test.com")
    assert validate_token(token) is True

def test_validate_tampered_token():
    token = generate_token(user_id=1, email="user@test.com")
    tampered = token[:-5] + "xxxxx"
    assert validate_token(tampered) is False

def test_validate_expired_token():
    token = generate_token(user_id=1, email="user@test.com", expires_in=-1)
    assert validate_token(token) is False

Phase 4: Auth Service (Cycles 8-10)

Cycle 8: Registration

# tests/test_auth_service.py

def test_register_creates_user():
    service = AuthService()
    user = service.register("user@test.com", "ValidPass1!")
    assert user["email"] == "user@test.com"
    assert "id" in user
    assert "password" not in user  # Never return password

def test_register_duplicate_email_raises():
    service = AuthService()
    service.register("user@test.com", "ValidPass1!")
    with pytest.raises(ValueError, match="already registered"):
        service.register("user@test.com", "AnotherPass1!")

def test_register_invalid_email_raises():
    service = AuthService()
    with pytest.raises(ValueError, match="Invalid email"):
        service.register("not-an-email", "ValidPass1!")

Cycle 9: Login

def test_login_returns_token():
    service = AuthService()
    service.register("user@test.com", "ValidPass1!")
    result = service.login("user@test.com", "ValidPass1!")
    assert "token" in result
    assert isinstance(result["token"], str)

def test_login_wrong_password_raises():
    service = AuthService()
    service.register("user@test.com", "ValidPass1!")
    with pytest.raises(ValueError, match="Invalid credentials"):
        service.login("user@test.com", "WrongPass1!")

def test_login_nonexistent_user_raises():
    service = AuthService()
    with pytest.raises(ValueError, match="Invalid credentials"):
        service.login("noone@test.com", "SomePass1!")

Cycle 10: Access validation

def test_validate_access_with_valid_token():
    service = AuthService()
    service.register("user@test.com", "ValidPass1!")
    result = service.login("user@test.com", "ValidPass1!")
    user = service.validate_access(result["token"])
    assert user["email"] == "user@test.com"

def test_validate_access_with_invalid_token_raises():
    service = AuthService()
    with pytest.raises(ValueError, match="Invalid token"):
        service.validate_access("fake-token")

Step-by-Step Process

For each cycle:

  1. Write the tests (RED)

    pytest tests/test_[module].py -v
    # → FAILED (red ✅ — this is what's expected)
  2. Prompt Claude Code (GREEN)

    Implement [function/class] in auth/[module].py so these tests 
    pass: [paste the tests]. 
    Don't use external dependencies. 
    Use only Python's stdlib.
    
  3. Evaluate the implementation

    pytest tests/test_[module].py -v
    # → All green? Evaluate the code's quality
    # → Anything red? Apply the decision framework
  4. Refactor (REFACTOR)

    Refactor [function] to improve readability.
    The tests must keep passing.
    
  5. Document the cycle — Note down:

    • Did Claude Code pass the tests on the first try?
    • Did you need to give more context?
    • Did you find any spec refinement?
    • Did you refactor anything?

Success Criteria

Your project is complete when:

  • ✅ pytest tests/ -v → all green
  • ✅ 25+ tests in total (validators + hasher + tokens + auth service)
  • ✅ 8+ documented TDD cycles
  • ✅ At least 1 documented case of spec refinement
  • ✅ At least 1 case where Claude Code needed more context
  • ✅ At least 1 documented refactoring

Evaluation Rubric (100 points)

Tests and Functionality (40 points)

  • (15 pts) All the tests pass
  • (10 pts) Coverage of the happy path, edge cases, and error handling
  • (10 pts) Independent and descriptive tests
  • (5 pts) 25+ tests in total

The TDD Process (35 points)

  • (15 pts) Evidence of red→green→refactor cycles (documentation)
  • (10 pts) At least 1 documented spec refinement
  • (10 pts) At least 1 case of intervention/iteration with Claude Code

Code Quality (15 points)

  • (5 pts) Clean and readable code
  • (5 pts) Type hints on the main functions
  • (5 pts) Small, focused functions (the result of refactoring)

Organization (10 points)

  • (5 pts) A correct file structure
  • (5 pts) Correct imports and no external dependencies

Extra Credit (up to +10 points)

  • (+5 pts) An E2E test that validates register → login → validate_access
  • (+5 pts) A parametrize test that validates multiple email formats

Common Mistakes

Mistake 1: Writing all the tests at once

Cause: Writing 30 tests before any implementation.

Solution: Work in phases. Implement one complete phase before starting the next. Within each phase, 2-4 tests per cycle is the sweet spot.

Mistake 2: Not documenting the cycles

Cause: Just writing code without recording the process.

Solution: After each cycle, write a line: "Cycle N: [test] → Claude passed/failed → [action] → green." The documentation is part of the deliverable.

Mistake 3: Tests that verify the implementation, not the behavior

Cause: assert hasher.algorithm == "sha256" verifies an internal implementation detail.

Solution: assert verify_password("pass", hashed) is True verifies behavior. Test WHAT it does, not HOW it does it.

Mistake 4: Skipping the refactoring

Cause: "The tests pass, I'm done."

Solution: Refactoring is part of the cycle. After green, always ask yourself: "Can I improve this code without breaking the tests?"

Mistake 5: Confusing spec refinement with an incorrect test

Cause: A test fails and you delete it instead of analyzing why.

Solution: If a test fails, ask yourself: "Is my spec incomplete (add more tests) or incorrect (modify the test)?" Deleting tests without reflection loses information.


Resources for the Project

  1. hashlib Documentation - Hashing with the stdlib
  2. secrets Module - Generating secure tokens
  3. pytest Documentation - pytest reference
  4. Kent Beck: TDD by Example - The reference book
  5. Martin Fowler: Refactoring - A catalog of refactorings

Connection with the Next Module

What you built today expands in the following modules:

  • Module 5 (Coverage): You'll measure your auth system's coverage and discover the tests you didn't write
  • Module 6 (Mocking): You'll learn to mock external services if your auth needs to verify an email via an API
  • Module 8 (Final Project): Every feature of the final app is built with this same workflow, but at a larger scale

You completed your first real feature with TDD. The workflow you experienced here is the same one you'll use professionally — only the scale changes.


Module 4, Capsule 06 — Testing with Claude Code Guide Your first complete feature built with agentic TDD