Módulo 1: Spec-First Methodology y TDD con AI
Por Qué TDD Importa Más con AI que Sin AI
Por Qué TDD Importa Más con AI que Sin AI
Descripción de la cápsula
Claude Code puede generar 200 líneas de código en 30 segundos. ¿Cuánto tiempo te tomaría revisar esas 200 líneas para verificar que son correctas? ¿Y si genera 2,000 líneas en una sesión? La velocidad de generación de código con AI es un superpoder — pero sin validación automática, es un superpoder peligroso.
Esta cápsula explora el problema central que motiva toda la guía: la velocidad de AI amplifica tanto código correcto como bugs. Más código por minuto significa más potenciales bugs por minuto. La solución no es "generar menos código" (eso sería absurdo) — es tener un sistema de validación que escale con la velocidad de generación. Ese sistema son los tests.
Al final de esta cápsula, entenderás por qué TDD no es un nice-to-have cuando trabajas con AI — es una necesidad operativa. Y tendrás argumentos concretos para convencer a tu equipo de adoptarlo.
El Problema: Velocidad Sin Validación
Antes de AI: El ritmo humano tenía límites naturales
Cuando escribes código manualmente, tu velocidad de producción es limitada:
Desarrollo manual típico:
- 50-100 líneas de código productivo por hora
- Review mental mientras escribes
- Compilas/ejecutas frecuentemente
- Bugs se detectan "en el momento"
Este ritmo tiene una ventaja oculta: la velocidad limitada actúa como buffer de calidad. Mientras escribes línea por línea, tu cerebro hace review en tiempo real. No es perfecto, pero es un filtro natural.
Con AI: La velocidad rompe el buffer
Claude Code elimina el buffer de velocidad:
Desarrollo con Claude Code típico:
- 200-500 líneas de código en minutos
- Generación instantánea (no escribes, recibes)
- No hay "review mientras escribes" — el código llega completo
- Bugs se esconden en código que "se ve correcto"
El problema no es que Claude Code genere mal código. El código generado por AI es generalmente bueno para el happy path. El problema es que genera código a una velocidad que supera tu capacidad de review manual.
La matemática del riesgo
Imagina que revisas código con un 95% de accuracy (detectas 19 de cada 20 bugs):
Desarrollo manual:
- 100 líneas/hora × 2% bug rate = 2 bugs/hora
- 95% detection rate = 1.9 bugs detectados
- 0.1 bugs escapan por hora ← Manejable
Desarrollo con AI:
- 500 líneas/hora × 2% bug rate = 10 bugs/hora
- 95% detection rate = 9.5 bugs detectados
- 0.5 bugs escapan por hora ← 5x más bugs en producción
Misma tasa de bugs, misma accuracy de review, pero 5x más bugs en producción. La velocidad amplifica el error residual.
La Ilusión del "Se Ve Bien"
Por qué el review visual falla con código generado por AI
Cuando Claude Code genera código, tiendes a hacer review visual:
# Claude Code genera esto para "implementa un sistema de descuentos":
def calculate_discount(price: float, discount_percent: float) -> float:
"""Calculate discounted price."""
if discount_percent < 0 or discount_percent > 100:
raise ValueError("Discount must be between 0 and 100")
discount_amount = price * (discount_percent / 100)
return price - discount_amount
Tu review visual: "Se ve bien. Valida el rango, calcula el descuento, retorna el precio. Aprobado."
¿Pero qué pasa con estos casos?
# ¿Qué pasa con price negativo?
calculate_discount(-50, 10) # Retorna -45.0 ← ¿Es correcto?
# ¿Qué pasa con price = 0?
calculate_discount(0, 50) # Retorna 0.0 ← OK, pero ¿debería aceptar precio 0?
# ¿Qué pasa con floats muy grandes?
calculate_discount(1e308, 50) # ¿Overflow?
# ¿Qué pasa con discount_percent = 100?
calculate_discount(100, 100) # Retorna 0.0 ← ¿Producto gratis es válido?
# ¿Qué pasa con tipos incorrectos?
calculate_discount("fifty", 10) # TypeError no manejado
El review visual detectó que el happy path funciona. Los tests habrían detectado que 5 edge cases no están manejados.
El sesgo de confirmación con código AI
Hay un sesgo cognitivo especialmente peligroso con código generado por AI: tiendes a buscar razones por las que el código es correcto en vez de buscar razones por las que podría fallar.
Cuando escribes código tú mismo, estás íntimamente consciente de las decisiones que tomaste y las que evitaste. Cuando recibes código de Claude Code, ves el resultado pero no el proceso — y el código "se ve profesional" porque la AI genera código bien formateado y bien nombrado.
Review de código propio:
- "Aquí no manejé el caso de None... debería agregar eso"
- "Esta validación no cubre strings vacíos"
- Conoces las debilidades porque las creaste
Review de código de AI:
- "Se ve limpio"
- "Los nombres de variables son buenos"
- "Tiene docstring"
- Asumes que "si se ve profesional, es correcto"
Lo Que Pasa Sin Tests: El Anti-Patrón "Genera y Reza"
El workflow que NO funciona
Este es el workflow más común entre developers que usan AI sin testing:
1. Pides a Claude Code: "Implementa autenticación con JWT"
2. Claude Code genera ~150 líneas de código
3. Review visual: "Se ve bien, tiene login, register, tokens"
4. Copias al proyecto
5. Pruebas manualmente con curl/Postman
6. "Funciona con mi caso de prueba"
7. Push a main
8. 3 días después: bug en producción — tokens no expiran correctamente
El problema no es en el paso 2 (la generación). Es en los pasos 3-6 (la validación).
El contra-ejemplo: Lo que pasa CON tests
1. Escribes tests ANTES de pedir implementación:
- test_register_creates_user
- test_register_duplicate_email_fails
- test_login_returns_valid_jwt
- test_jwt_expires_after_timeout
- test_expired_jwt_is_rejected
- test_invalid_jwt_returns_401
2. Das los tests como contexto a Claude Code:
"Implementa autenticación para que estos tests pasen"
3. Claude Code genera implementación
4. Ejecutas pytest → 4/6 tests pasan, 2 fallan
5. Claude Code itera sobre los fallos
6. pytest → 6/6 tests pasan
7. Push a main con confianza
La diferencia: En el segundo workflow, el test test_jwt_expires_after_timeout habría forzado a Claude Code a implementar la expiración correctamente — porque el test habría fallado si no lo hiciera.
Tres Razones Concretas por las que TDD + AI > AI Sola
Razón 1: Los tests eliminan ambigüedad
Cuando le dices a Claude Code "implementa autenticación con JWT", hay docenas de decisiones implícitas:
- ¿Cuánto dura el token? ¿1 hora? ¿24 horas? ¿1 semana?
- ¿Qué claims incluye el JWT? ¿Solo user_id? ¿También role?
- ¿Cómo maneja tokens expirados? ¿401? ¿403?
- ¿Acepta refresh tokens?
- ¿Qué pasa con tokens malformados?
Sin tests: Claude Code toma estas decisiones por ti. A veces acierta, a veces no.
Con tests: Tus tests definen las respuestas:
def test_jwt_expires_in_one_hour():
token = create_token(user_id=1)
payload = decode_token(token)
assert payload["exp"] - payload["iat"] == 3600
def test_jwt_includes_user_id_and_role():
token = create_token(user_id=1, role="admin")
payload = decode_token(token)
assert payload["user_id"] == 1
assert payload["role"] == "admin"
def test_expired_token_raises_error():
expired_token = create_token(user_id=1, expires_delta=-1)
with pytest.raises(TokenExpiredError):
decode_token(expired_token)
Los tests son especificación no-ambigua. Claude Code no tiene que adivinar — tus tests le dicen exactamente qué debe pasar.
Razón 2: Los tests escalan con la velocidad de AI
El review manual no escala:
Claude Code genera 500 líneas → Review manual: 30-60 minutos
Claude Code genera 2000 líneas → Review manual: 2-4 horas (¿realmente lo harás?)
Claude Code genera 5000 líneas → Review manual: Abandonas y "confías"
Los tests SÍ escalan:
500 líneas con 20 tests → pytest: 2 segundos
2000 líneas con 80 tests → pytest: 5 segundos
5000 líneas con 200 tests → pytest: 15 segundos
pytest no se cansa, no tiene sesgo de confirmación, y no dice "se ve bien" cuando hay un bug.
Razón 3: Los tests permiten iteration loops automáticos
Este es el diferenciador más poderoso del TDD con AI. Sin tests, cuando Claude Code genera código incorrecto, el ciclo es:
Sin tests:
1. Claude genera → 2. Tú revisas → 3. "Esto está mal" →
4. Explicas el problema en lenguaje natural →
5. Claude regenera → 6. Tú revisas de nuevo → ...
Con tests, el ciclo es:
Con tests:
1. Claude genera → 2. pytest → 3. "2 tests failed" →
4. Claude lee el output de pytest →
5. Claude corrige automáticamente → 6. pytest → "All passed" ✅
La diferencia: Con tests, Claude Code recibe feedback preciso y puede auto-corregirse. Sin tests, dependes de tu capacidad de explicar el error — y tu explicación puede ser ambigua.
Comparación: Test-After vs Test-First con AI
| Criterio | Test-After (tradicional) | Test-First (spec-first) |
|---|---|---|
| Cuándo escribes tests | Después de implementar | Antes de implementar |
| Rol del test | Verificación | Especificación |
| Ambigüedad | Claude adivina requisitos | Tests definen requisitos |
| Feedback | Manual (review visual) | Automático (pytest) |
| Edge cases | Los descubres después | Los defines antes |
| Iteration loops | Manuales (tú explicas) | Automáticos (pytest output) |
| Confianza | "Se ve bien" | "Tests pasan" |
¿Cuándo usar test-after? Cuando tienes código legacy sin tests y necesitas agregar cobertura retroactivamente (se cubre en Módulo 5).
¿Cuándo usar test-first? Siempre que estés construyendo algo nuevo con Claude Code. Es el default de esta guía.
Trade-off: Test-first requiere más esfuerzo inicial (definir tests antes de ver código). Pero el ROI es masivo: menos bugs, menos review manual, iteration loops automáticos.
Conexión con Proyecto
En el Spec-first mini-app (proyecto de este módulo):
- Verás en acción cómo la ambigüedad desaparece cuando defines tests primero (Razón 1)
- Experimentarás que pytest valida en segundos lo que tomaría minutos revisar manualmente (Razón 2)
- Ejecutarás tu primer iteration loop automático con Claude Code (Razón 3)
Todo lo que aprendes hoy se aplica en cada proyecto de la guía — y en cada proyecto futuro con Claude Code.
Troubleshooting
Problema 1: "Pero yo SÍ reviso bien el código de Claude Code"
Causa: Sobreestimación de la capacidad de review manual. Estudios muestran que incluso developers senior detectan ~60-85% de bugs en code review, no 100%.
Solución: No se trata de reemplazar tu review — se trata de complementarlo. Tests + review visual > review visual solo. Los tests detectan lo que tu review no ve (especialmente edge cases y regresiones).
Problema 2: "Escribir tests primero toma más tiempo"
Causa: Perspectiva de corto plazo. Escribir tests primero toma 5-10 minutos más por feature.
Solución: Mide el tiempo total incluyendo debugging posterior. Sin tests, pasas 30-60 minutos debugeando bugs que habrías atrapado con 5 minutos de tests. El ROI es 3-6x.
Problema 3: "No sé qué testear todavía — necesito ver el código primero"
Causa: Confundir "no sé cómo implementar" con "no sé qué debería pasar." No necesitas saber el cómo para definir el qué.
Solución: Piensa en comportamiento, no en implementación. No necesitas saber cómo se implementa calculate_discount para saber que calculate_discount(100, 10) debe retornar 90. Define QUÉ debe pasar — Claude Code se encarga del CÓMO.
Problema 4: "Claude Code ya genera tests si se lo pido"
Causa: Confusión entre test-generation y spec-first. Claude Code puede generar tests, sí — pero esos tests verifican la implementación que YA generó. Son test-after, no test-first. Validan que el código hace lo que hace, no que hace lo que TÚ quieres.
Solución: Los tests que defines TÚ reflejan tu intención. Los tests que Claude Code genera reflejan la implementación. Ambos son útiles, pero solo los tuyos son spec-first.
Ejercicios
Ejercicio 1: Identificar el riesgo (Fácil)
Claude Code genera esta función. Haz review visual y lista 3 edge cases que NO están cubiertos:
def calculate_bmi(weight_kg: float, height_m: float) -> str:
bmi = weight_kg / (height_m ** 2)
if bmi < 18.5:
return "underweight"
elif bmi < 25:
return "normal"
elif bmi < 30:
return "overweight"
else:
return "obese"
Ver solución
Edge cases no cubiertos:
- height_m = 0: División por cero (
ZeroDivisionError) - weight_kg negativo: BMI negativo no tiene sentido médico
- height_m negativo: Height al cuadrado da positivo, pero el input es inválido
- Tipos incorrectos:
calculate_bmi("heavy", 1.75)→TypeError - Valores extremos:
calculate_bmi(1, 0.01)→ BMI = 10000 (no realista)
# Tests que habrían forzado el manejo de estos casos:
def test_bmi_zero_height_raises_error():
with pytest.raises(ValueError, match="height must be positive"):
calculate_bmi(70, 0)
def test_bmi_negative_weight_raises_error():
with pytest.raises(ValueError, match="weight must be positive"):
calculate_bmi(-70, 1.75)
def test_bmi_negative_height_raises_error():
with pytest.raises(ValueError, match="height must be positive"):
calculate_bmi(70, -1.75)
Explicación: El review visual se enfocó en la lógica del BMI (que es correcta). Los tests habrían forzado validación de inputs.
Ejercicio 2: Escribir el test primero (Fácil)
Vas a pedirle a Claude Code que implemente una función is_valid_email(email: str) -> bool. ANTES de pedir la implementación, escribe 5 tests que definan el comportamiento esperado.
Ver solución
import pytest
def test_valid_email_returns_true():
assert is_valid_email("user@example.com") == True
def test_email_without_at_returns_false():
assert is_valid_email("userexample.com") == False
def test_email_without_domain_returns_false():
assert is_valid_email("user@") == False
def test_empty_string_returns_false():
assert is_valid_email("") == False
def test_email_with_spaces_returns_false():
assert is_valid_email("user @example.com") == False
Explicación: Estos 5 tests definen el contrato de is_valid_email sin saber cómo se implementa. No necesitas saber si usa regex, parsing manual, o una biblioteca — solo defines qué debe pasar con cada input. Cuando des estos tests a Claude Code, la implementación está forzada a manejar estos casos.
Ejercicio 3: Comparar workflows (Medio)
Imagina que necesitas una función parse_csv_line(line: str) -> list[str] que parsea una línea CSV. Describe los dos workflows (con tests y sin tests) y explica qué podría salir mal en cada uno.
Ver solución
Workflow sin tests:
1. "Claude, implementa parse_csv_line que parsee una línea CSV"
2. Claude genera función con split(",")
3. Review visual: "Se ve bien"
4. En producción: falla con "John, Jr.,30,New York" (coma dentro del nombre)
Workflow con tests:
def test_simple_csv():
assert parse_csv_line("a,b,c") == ["a", "b", "c"]
def test_csv_with_quotes():
assert parse_csv_line('"John, Jr.",30,NYC') == ["John, Jr.", "30", "NYC"]
def test_empty_fields():
assert parse_csv_line("a,,c") == ["a", "", "c"]
def test_empty_line():
assert parse_csv_line("") == []
def test_single_field():
assert parse_csv_line("hello") == ["hello"]
1. Das los tests a Claude Code
2. Claude genera implementación que maneja quotes, campos vacíos, etc.
3. pytest → All pass
4. Confianza real basada en tests, no en "se ve bien"
Explicación: Sin tests, Claude Code probablemente usaría line.split(",") que falla con campos entrecomillados. Con tests, el test test_csv_with_quotes habría forzado a Claude Code a usar csv.reader o una implementación que maneje quotes correctamente.
Ejercicio 4: Calcular el ROI del testing (Medio)
Tienes un proyecto donde Claude Code genera ~1000 líneas de código por sesión. Asumiendo un bug rate del 3% y un costo de fix de 30 minutos por bug en producción vs 5 minutos si se detecta con tests:
- ¿Cuántos bugs potenciales por sesión?
- Si tu review manual detecta 80%, ¿cuántos escapan?
- ¿Cuánto tiempo pierdes en fixes de producción?
- Si escribir tests toma 20 minutos por sesión, ¿cuál es el ROI?
Ver solución
1. Bugs potenciales: 1000 × 3% = 30 bugs/sesión
2. Bugs que escapan review: 30 × 20% = 6 bugs llegan a producción
3. Tiempo de fix en producción: 6 × 30 min = 180 min = 3 horas
4. Con tests (asumiendo 95% detection):
- Tests detectan: 30 × 95% = 28.5 bugs
- Fix con test feedback: 28.5 × 5 min = 142.5 min
- Bugs que escapan: 30 × 5% = 1.5 bugs
- Fix en producción: 1.5 × 30 min = 45 min
Tiempo total SIN tests: 180 min de fixes en producción
Tiempo total CON tests: 20 min (escribir) + 142.5 min (fix rápido) + 45 min (producción) = 207.5 min
Pero el tiempo de "fix rápido" se absorbe en el desarrollo
(Claude Code itera automáticamente con test feedback)
Tiempo NETO adicional: 20 min de escribir tests
Tiempo AHORRADO: 180 - 45 = 135 min de debugging en producción
ROI: 135 min ahorrados / 20 min invertidos = 6.75x
Explicación: El ROI real es incluso mayor porque no contabiliza el costo reputacional de bugs en producción, el tiempo de debugging en contexto (reproducir, diagnosticar, fix, deploy), ni el valor de la confianza para refactoring futuro.
Ejercicio 5: Identificar test-after vs test-first (Difícil)
Lee estos dos escenarios y determina cuál es test-after y cuál es test-first. Explica por qué uno produce resultados más confiables.
Escenario A:
1. "Claude, implementa un rate limiter que permita max 100 requests por minuto"
2. Claude genera implementación
3. "Claude, ahora escribe tests para el rate limiter que generaste"
4. Claude genera tests que pasan con la implementación
Escenario B:
1. Escribes: test_allows_100_requests_per_minute
2. Escribes: test_blocks_101st_request
3. Escribes: test_resets_after_one_minute
4. Escribes: test_tracks_per_client_ip
5. "Claude, implementa un rate limiter que pase estos tests"
6. Claude genera implementación
Ver solución
Escenario A: Test-after. Claude Code genera tests que verifican lo que la implementación HACE, no lo que DEBERÍA hacer. Si la implementación tiene un bug (por ejemplo, no resetea el contador después de un minuto), los tests generados no van a testear eso — porque están basados en la implementación, no en los requisitos.
Escenario B: Test-first (spec-first). Los tests definen el comportamiento esperado ANTES de que exista implementación. test_resets_after_one_minute y test_tracks_per_client_ip son requisitos que podrían no haberse implementado si no se hubieran especificado como tests.
Por qué B produce resultados más confiables:
- Los tests de B reflejan la intención del developer (lo que QUIERE)
- Los tests de A reflejan el comportamiento actual (lo que la AI GENERÓ)
- Si la implementación de A tiene un bug, los tests de A probablemente no lo detectan
- Si la implementación de B tiene un bug, los tests de B lo detectan inmediatamente
Regla: Tests generados por AI sobre código generado por AI = circular validation (no confiable). Tests escritos por humano sobre código generado por AI = real validation.
Resumen
En esta cápsula aprendiste:
- ✅ La velocidad de AI amplifica tanto código bueno como bugs — el review manual no escala con esa velocidad
- ✅ El review visual sufre de sesgo de confirmación: código AI "se ve profesional" pero puede tener edge cases sin manejar
- ✅ El workflow "genera y reza" es el anti-patrón más común y más costoso
- ✅ TDD con AI elimina ambigüedad (tests definen requisitos), escala con velocidad (pytest valida en segundos), y permite iteration loops automáticos
- ✅ Test-first (spec-first) produce resultados más confiables que test-after porque los tests reflejan tu intención, no la implementación de la AI
- ✅ El ROI del testing con AI es significativo: minutos invertidos ahorran horas de debugging
Próxima cápsula: Spec-First Methodology — la metodología Tweag y la inversión de control con Claude Code.
Recursos Adicionales
- Tweag: Spec-Driven Development with LLMs - Origen de la metodología spec-first aplicada a desarrollo con LLMs
- pytest: Getting Started - Setup básico de pytest (lo usarás desde el módulo 2)
- Anthropic: Best Practices for Claude Code - Mejores prácticas oficiales para trabajar con Claude Code
- Martin Fowler: Is TDD Dead? - Debate clásico sobre TDD — contexto útil
- Greg Wilson: Software Engineering's Greatest Hits - Evidencia empírica sobre prácticas de desarrollo que funcionan
- Code Review Research by Microsoft - Investigación sobre las limitaciones del code review manual
Módulo 1, Cápsula 02 — Testing with Claude Code Guide La velocidad sin validación es deuda técnica acelerada