Módulo 4: TDD Workflow Completo
Proyecto del Módulo: Feature Completa con TDD
Proyecto del Módulo: Feature Completa con TDD
Descripción del proyecto
Has aprendido el ciclo TDD completo con Claude Code: escribir tests rojos, dejar que Claude implemente, evaluar, refactorizar. Ahora vas a experimentar el workflow real construyendo una feature completa de principio a fin: un sistema de autenticación con registro, login, y validación de tokens.
Cada aspecto del sistema empieza como un test que falla. Claude Code implementa. Evalúas. Refactorizas. Pasas al siguiente test. Al final, tendrás un sistema funcional construido ciclo por ciclo — y habrás experimentado spec refinement, errores de Claude Code, y el juicio de cuándo intervenir.
La clave no es el código final — es el proceso. Vas a documentar cada ciclo TDD para internalizar el ritmo.
Objetivo del Proyecto
Construir un sistema de autenticación completo usando TDD con Claude Code, experimentando múltiples ciclos red-green-refactor consecutivos.
Al completar este proyecto:
- ✅ Habrás ejecutado 8+ ciclos TDD completos
- ✅ Habrás experimentado spec refinement (ajustar tests al descubrir gaps)
- ✅ Habrás manejado situaciones donde Claude Code falla y necesita más contexto
- ✅ Habrás refactorizado código con tests como safety net
- ✅ Tendrás un sistema de autenticación funcional construido test-first
Especificaciones Técnicas
Stack Tecnológico
- Lenguaje: Python 3.10+
- Testing: pytest
- Hashing: hashlib (stdlib) — no dependencias externas para simplificar
- Tokens: secrets + json (simulación simple de JWT sin dependencias)
- AI: Claude Code
Setup Inicial
mkdir auth-tdd-project
cd auth-tdd-project
python -m venv venv
source venv/bin/activate
pip install pytest
Estructura del Proyecto
auth-tdd-project/
├── auth/
│ ├── __init__.py
│ ├── validators.py ← Validación de email y password (tú creas con TDD)
│ ├── hasher.py ← Hashing de passwords (tú creas con TDD)
│ ├── token_manager.py ← Generación y validación de tokens (tú creas con TDD)
│ └── auth_service.py ← Servicio de autenticación (tú creas con TDD)
├── tests/
│ ├── __init__.py
│ ├── test_validators.py
│ ├── test_hasher.py
│ ├── test_token_manager.py
│ └── test_auth_service.py
└── requirements.txt
Los Ciclos TDD
Fase 1: Validadores (Ciclos 1-3)
Ciclo 1: Validación de email
# tests/test_validators.py — Escribe PRIMERO
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
Tu turno:
- Escribe estos tests
- Ejecuta
pytest tests/test_validators.py -v— todo rojo - Prompt a Claude Code: "Implementa
validate_emailenauth/validators.pypara que pasen estos tests." - Evalúa la implementación
- Refactoriza si es necesario
Ciclo 2: Validación de password
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
Ciclo 3: Mensajes de error específicos
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: En el Ciclo 2, validate_password retorna bool. Ahora en el Ciclo 3, necesitas una función diferente que retorne detalles. Esto es spec refinement — descubres durante el proceso que necesitas más granularidad.
Fase 2: Password Hashing (Ciclos 4-5)
Ciclo 4: Hash básico
# 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
Ciclo 5: Verificación de password
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
Fase 3: Token Manager (Ciclos 6-7)
Ciclo 6: Generación de tokens
# 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
Ciclo 7: Validación de tokens
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
Fase 4: Auth Service (Ciclos 8-10)
Ciclo 8: Registro
# 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!")
Ciclo 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!")
Ciclo 10: Validación de acceso
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")
Proceso Paso a Paso
Para cada ciclo:
-
Escribe los tests (RED)
pytest tests/test_[module].py -v # → FAILED (rojo ✅ — esto es lo esperado) -
Prompt a Claude Code (GREEN)
Implementa [función/clase] en auth/[module].py para que pasen estos tests: [pega los tests]. No uses dependencias externas. Usa solo la stdlib de Python. -
Evalúa la implementación
pytest tests/test_[module].py -v # → ¿Todo verde? Evalúa la calidad del código # → ¿Algo rojo? Aplica el framework de decisión -
Refactoriza (REFACTOR)
Refactoriza [función] para mejorar legibilidad. Los tests deben seguir pasando. -
Documenta el ciclo — Anota:
- ¿Claude Code pasó los tests al primer intento?
- ¿Necesitaste dar más contexto?
- ¿Encontraste spec refinement?
- ¿Refactorizaste algo?
Criterios de Éxito
Tu proyecto está completo cuando:
- ✅
pytest tests/ -v→ todos green - ✅ 25+ tests en total (validadores + hasher + tokens + auth service)
- ✅ 8+ ciclos TDD documentados
- ✅ Al menos 1 caso de spec refinement documentado
- ✅ Al menos 1 caso donde Claude Code necesitó más contexto
- ✅ Al menos 1 refactoring documentado
Rúbrica de Evaluación (100 puntos)
Tests y Funcionalidad (40 puntos)
- (15 pts) Todos los tests pasan
- (10 pts) Cobertura de happy path, edge cases, y error handling
- (10 pts) Tests independientes y descriptivos
- (5 pts) 25+ tests en total
Proceso TDD (35 puntos)
- (15 pts) Evidencia de ciclos red→green→refactor (documentación)
- (10 pts) Al menos 1 spec refinement documentado
- (10 pts) Al menos 1 caso de intervención/iteración con Claude Code
Calidad del Código (15 puntos)
- (5 pts) Código limpio y legible
- (5 pts) Type hints en las funciones principales
- (5 pts) Funciones pequeñas y enfocadas (resultado de refactoring)
Organización (10 puntos)
- (5 pts) Estructura de archivos correcta
- (5 pts) Imports correctos y sin dependencias externas
Extra Credit (hasta +10 puntos)
- (+5 pts) E2E test que valida register → login → validate_access
- (+5 pts) Test parametrize que valide múltiples formatos de email
Errores Comunes
Error 1: Escribir todos los tests de una vez
Causa: Escribir 30 tests antes de cualquier implementación.
Solución: Trabajo por fases. Implementa una fase completa antes de empezar la siguiente. Dentro de cada fase, 2-4 tests por ciclo es el sweet spot.
Error 2: No documentar los ciclos
Causa: Solo escribir código sin registrar el proceso.
Solución: Después de cada ciclo, escribe una línea: "Ciclo N: [test] → Claude pasó/falló → [acción] → verde." La documentación es parte del entregable.
Error 3: Tests que verifican implementación, no comportamiento
Causa: assert hasher.algorithm == "sha256" verifica implementación interna.
Solución: assert verify_password("pass", hashed) is True verifica comportamiento. Testea QUÉ hace, no CÓMO lo hace.
Error 4: Saltarse el refactoring
Causa: "Los tests pasan, terminé."
Solución: El refactoring es parte del ciclo. Después de verde, siempre pregúntate: "¿Puedo mejorar este código sin romper tests?"
Error 5: Confundir spec refinement con test incorrecto
Causa: Un test falla y lo borras en vez de analizar por qué.
Solución: Si un test falla, pregúntate: "¿Mi spec está incompleta (agregar más tests) o incorrecta (modificar el test)?" Borrar tests sin reflexión pierde información.
Recursos para el Proyecto
- hashlib Documentation - Hashing con stdlib
- secrets Module - Generación de tokens seguros
- pytest Documentation - Referencia de pytest
- Kent Beck: TDD by Example - El libro de referencia
- Martin Fowler: Refactoring - Catálogo de refactorings
Conexión con Siguiente Módulo
Lo que construiste hoy se expande en los siguientes módulos:
- Módulo 5 (Coverage): Medirás la cobertura de tu sistema de auth y descubrirás los tests que no escribiste
- Módulo 6 (Mocking): Aprenderás a mockear servicios externos si tu auth necesita verificar email via API
- Módulo 8 (Proyecto Final): Cada feature de la app final se construye con este mismo workflow, pero a mayor escala
Completaste tu primera feature real con TDD. El workflow que experimentaste aquí es el mismo que usarás profesionalmente — solo cambia la escala.
Módulo 4, Cápsula 06 — Testing with Claude Code Guide Tu primera feature completa construida con TDD agentic