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:
- Write these tests
- Run
pytest tests/test_validators.py -v— all red - Prompt to Claude Code: "Implement
validate_emailinauth/validators.pyso these tests pass." - Evaluate the implementation
- 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:
-
Write the tests (RED)
pytest tests/test_[module].py -v # → FAILED (red ✅ — this is what's expected) -
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. -
Evaluate the implementation
pytest tests/test_[module].py -v # → All green? Evaluate the code's quality # → Anything red? Apply the decision framework -
Refactor (REFACTOR)
Refactor [function] to improve readability. The tests must keep passing. -
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
- hashlib Documentation - Hashing with the stdlib
- secrets Module - Generating secure tokens
- pytest Documentation - pytest reference
- Kent Beck: TDD by Example - The reference book
- 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