Módulo 1: Spec-First Methodology y TDD con AI
Spec-First Methodology: Tests como Especificación
Spec-First Methodology: Tests como Especificación
Descripción de la cápsula
En la cápsula anterior entendiste POR QUÉ TDD importa más con AI. Ahora vas a aprender el CÓMO: la metodología spec-first. La idea central es una inversión de control radical — en vez de decirle a Claude Code "implementa esto" en lenguaje natural (ambiguo), le dices "aquí están los tests que definen qué debe pasar" (preciso, ejecutable, verificable).
Spec-first no es una invención de esta guía. Está basado en el trabajo de Tweag con desarrollo asistido por LLMs, donde descubrieron que los tests son el lenguaje más efectivo para comunicar requisitos a un agente AI. Esta cápsula te enseña la metodología concreta: cómo invertir el control, qué cambia en tu forma de pensar, y cómo estructurar el workflow con Claude Code.
Al final, tendrás un framework mental claro: tú eres el arquitecto de comportamiento (defines qué debe pasar), Claude Code es el constructor (implementa cómo pasa). Los tests son el plano que conecta ambos roles.
La Inversión de Control
TDD clásico vs Agentic TDD
En TDD clásico, el developer hace todo:
TDD Clásico (sin AI):
┌────────────┐
│ Developer │
│ │
│ 1. Escribe │─── test ──→ [Falla] RED
│ test │
│ │
│ 2. Escribe │─── implementación ──→ [Pasa] GREEN
│ código │
│ │
│ 3. Mejora │─── refactor ──→ [Sigue pasando] REFACTOR
│ código │
└────────────┘
En agentic TDD, los roles se dividen:
Agentic TDD (con Claude Code):
┌────────────┐ ┌─────────────┐
│ Developer │ │ Claude Code │
│ │ │ │
│ 1. Define │── test ─→│ 2. Implementa ──→ [Pasa] GREEN
│ spec │ │ código │
│ (test) │ │ │
│ │ │ 3. Refactoriza ──→ [Sigue pasando]
│ 4. Valida │←─ pytest─│ │
│ │ │ │
│ 5. Ajusta │── test ─→│ 6. Itera │
│ spec │ │ │
└────────────┘ └─────────────┘
La inversión: En TDD clásico, escribir la implementación es TU trabajo más laborioso. En agentic TDD, ese trabajo lo hace Claude Code. Tu trabajo más importante pasa a ser definir la spec (los tests).
¿Qué significa "tests como especificación"?
Una especificación es un documento que define qué debe hacer un sistema. Tradicionalmente, se escribe en lenguaje natural:
Especificación en lenguaje natural (ambigua):
"El sistema debe calcular descuentos. Los descuentos válidos están
entre 0% y 100%. El sistema debe manejar errores apropiadamente."
Problemas:
- ¿"Manejar errores apropiadamente" significa retornar None? ¿Lanzar excepción? ¿Retornar 0?
- ¿"Entre 0% y 100%" incluye 0 y 100, o los excluye?
- ¿Qué pasa con inputs no numéricos?
Ahora compara con una especificación como tests:
def test_discount_basic():
assert calculate_discount(100, 10) == 90.0
def test_discount_zero_percent():
assert calculate_discount(100, 0) == 100.0
def test_discount_hundred_percent():
assert calculate_discount(100, 100) == 0.0
def test_discount_negative_percent_raises():
with pytest.raises(ValueError, match="between 0 and 100"):
calculate_discount(100, -5)
def test_discount_over_hundred_raises():
with pytest.raises(ValueError, match="between 0 and 100"):
calculate_discount(100, 150)
def test_discount_negative_price_raises():
with pytest.raises(ValueError, match="price must be positive"):
calculate_discount(-50, 10)
Esta especificación:
- ✅ No es ambigua — cada test tiene un resultado exacto esperado
- ✅ Es ejecutable —
pytestla valida automáticamente - ✅ Es verificable — pasa o no pasa, sin interpretación
- ✅ Documenta edge cases explícitamente (0%, 100%, negativos)
- ✅ Define cómo se manejan errores (ValueError con mensaje específico)
El Framework Spec-First
Paso 1: Define el comportamiento como tests
Antes de tocar Claude Code, piensa en QUÉ debe pasar. No en CÓMO se implementa.
# Piensa en comportamiento, no en implementación:
# ❌ "Necesito una función que use regex para validar emails"
# (esto es implementación, no comportamiento)
# ✅ "Necesito una función donde:"
def test_valid_email():
assert is_valid_email("user@example.com") == True
def test_missing_at():
assert is_valid_email("userexample.com") == False
def test_missing_domain():
assert is_valid_email("user@") == False
def test_empty_string():
assert is_valid_email("") == False
def test_multiple_at_signs():
assert is_valid_email("user@@example.com") == False
Regla: Si estás pensando en regex, algoritmos, o estructuras de datos, estás pensando en implementación. Piensa en inputs y outputs.
Paso 2: Da los tests como contexto a Claude Code
No le digas "implementa un validador de email." Dale los tests:
Prompt a Claude Code:
"Tengo estos tests en test_email.py:
[pegar tests]
Implementa la función is_valid_email en email_validator.py
para que todos los tests pasen."
¿Por qué funciona? Claude Code recibe:
- ✅ El nombre de la función (
is_valid_email) - ✅ La firma de la función (
email: str -> bool) - ✅ Casos concretos de input/output
- ✅ Edge cases que DEBEN manejarse
- ✅ Comportamiento de error esperado
No hay ambigüedad. Claude Code tiene toda la información que necesita para implementar correctamente.
Paso 3: Ejecuta los tests
pytest test_email.py -v
test_email.py::test_valid_email PASSED
test_email.py::test_missing_at PASSED
test_email.py::test_missing_domain PASSED
test_email.py::test_empty_string PASSED
test_email.py::test_multiple_at_signs PASSED
========================= 5 passed in 0.01s =========================
Si todos pasan: la implementación cumple tu especificación. Si alguno falla: Claude Code itera.
Paso 4: Itera si es necesario
pytest test_email.py -v
test_email.py::test_valid_email PASSED
test_email.py::test_missing_at PASSED
test_email.py::test_missing_domain FAILED ← Este falla
test_email.py::test_empty_string PASSED
test_email.py::test_multiple_at_signs PASSED
Le das el output a Claude Code:
"test_missing_domain falló. Aquí está el output:
AssertionError: assert True == False
La función retorna True para 'user@' pero debería retornar False.
Corrige la implementación."
Claude Code lee el feedback preciso del test y corrige. No necesitas explicar el problema en lenguaje natural — pytest lo explica por ti.
La Analogía del Arquitecto y el Constructor
Tú eres el arquitecto. Defines los planos: cuántas habitaciones, qué dimensiones, dónde van las ventanas, qué materiales. No colocas ladrillos — defines qué debe existir.
Claude Code es el constructor. Recibe los planos y construye. Sabe colocar ladrillos, mezclar concreto, instalar tuberías. Pero sin planos, construye lo que le parece — y puede no ser lo que querías.
Los tests son los planos. Son precisos, verificables, y no ambiguos. Cada test es una instrucción: "esta habitación debe medir 4x5 metros" = "esta función debe retornar 90 cuando recibe 100 y 10."
Sin planos (sin tests):
Arquitecto: "Quiero una casa bonita"
Constructor: [construye algo]
Arquitecto: "No, eso no era lo que quería"
Constructor: [rehacer desde cero]
Con planos (con tests):
Arquitecto: [entrega planos detallados]
Constructor: [construye según planos]
Inspector: [verifica contra planos] ← pytest
✅ "Todo cumple con las especificaciones"
Spec-First en Práctica: Ejemplo Completo
Vamos a ver el ciclo spec-first completo para una función real. Imagina que necesitas un password strength checker.
1. Define la spec (tú escribes los tests)
# test_password_checker.py
import pytest
from password_checker import check_password_strength
class TestPasswordStrength:
"""Spec: Password strength checker"""
def test_strong_password(self):
"""Una password con 8+ chars, mayúscula, minúscula, número y símbolo es 'strong'"""
result = check_password_strength("MyP@ss1!")
assert result["strength"] == "strong"
def test_medium_password(self):
"""Una password con 8+ chars y 3 de 4 criterios es 'medium'"""
result = check_password_strength("MyPass12")
assert result["strength"] == "medium"
def test_weak_password(self):
"""Una password con menos de 8 chars es siempre 'weak'"""
result = check_password_strength("abc")
assert result["strength"] == "weak"
def test_empty_password(self):
"""Una password vacía es 'weak'"""
result = check_password_strength("")
assert result["strength"] == "weak"
def test_returns_criteria_met(self):
"""El resultado incluye qué criterios se cumplieron"""
result = check_password_strength("MyP@ss1!")
assert result["has_uppercase"] == True
assert result["has_lowercase"] == True
assert result["has_number"] == True
assert result["has_special"] == True
assert result["min_length"] == True
def test_only_lowercase_criteria(self):
"""Una password de solo minúsculas reporta correctamente"""
result = check_password_strength("abcdefgh")
assert result["has_uppercase"] == False
assert result["has_lowercase"] == True
assert result["has_number"] == False
assert result["has_special"] == False
def test_length_boundary_7_chars(self):
"""7 caracteres no cumple el mínimo de 8"""
result = check_password_strength("Aa1!567")
assert result["min_length"] == False
assert result["strength"] == "weak"
def test_length_boundary_8_chars(self):
"""8 caracteres cumple el mínimo"""
result = check_password_strength("Aa1!5678")
assert result["min_length"] == True
Observa: No sabes (ni te importa) cómo se implementa. Solo defines qué debe retornar para cada input.
2. Da la spec a Claude Code
"Tengo esta spec en test_password_checker.py:
[tests de arriba]
Crea password_checker.py con la función check_password_strength
que haga pasar todos los tests. La función recibe un string y
retorna un dict con 'strength' y los criterios individuales."
3. Claude Code implementa
Claude Code genera algo como:
# password_checker.py
import re
def check_password_strength(password: str) -> dict:
criteria = {
"has_uppercase": bool(re.search(r"[A-Z]", password)),
"has_lowercase": bool(re.search(r"[a-z]", password)),
"has_number": bool(re.search(r"\d", password)),
"has_special": bool(re.search(r"[!@#$%^&*(),.?\":{}|<>]", password)),
"min_length": len(password) >= 8,
}
criteria_count = sum([
criteria["has_uppercase"],
criteria["has_lowercase"],
criteria["has_number"],
criteria["has_special"],
])
if not criteria["min_length"]:
strength = "weak"
elif criteria_count >= 4:
strength = "strong"
elif criteria_count >= 3:
strength = "medium"
else:
strength = "weak"
return {"strength": strength, **criteria}
4. Valida con pytest
pytest test_password_checker.py -v
test_password_checker.py::TestPasswordStrength::test_strong_password PASSED
test_password_checker.py::TestPasswordStrength::test_medium_password PASSED
test_password_checker.py::TestPasswordStrength::test_weak_password PASSED
test_password_checker.py::TestPasswordStrength::test_empty_password PASSED
test_password_checker.py::TestPasswordStrength::test_returns_criteria_met PASSED
test_password_checker.py::TestPasswordStrength::test_only_lowercase_criteria PASSED
test_password_checker.py::TestPasswordStrength::test_length_boundary_7_chars PASSED
test_password_checker.py::TestPasswordStrength::test_length_boundary_8_chars PASSED
========================= 8 passed in 0.02s =========================
8/8 tests pasan. La implementación cumple tu especificación exactamente.
Comparación: Lenguaje Natural vs Tests como Spec
| Aspecto | Spec en lenguaje natural | Spec como tests |
|---|---|---|
| Precisión | "Manejar errores apropiadamente" | pytest.raises(ValueError, match="...") |
| Verificabilidad | Alguien lee y opina | pytest dice PASS/FAIL |
| Edge cases | Fácil olvidarlos | Cada test es un edge case explícito |
| Ambigüedad | "El password debe ser fuerte" | assert strength == "strong" para input específico |
| Mantenimiento | Se desactualiza silenciosamente | Si el código cambia y tests fallan, lo sabes |
| Comunicación con AI | Claude Code interpreta (puede errar) | Claude Code ejecuta (preciso) |
¿Cuándo usar lenguaje natural? Para dar contexto general y motivación. "Necesito un password checker porque los usuarios eligen passwords débiles." Esto ayuda a Claude Code a entender el dominio.
¿Cuándo usar tests? Para definir comportamiento específico. Qué retorna la función, cómo maneja errores, qué edge cases cubre. Esto es lo que Claude Code implementa.
Best practice: Combina ambos. Lenguaje natural para contexto + tests para especificación.
Conexión con Proyecto
En el Spec-first mini-app (proyecto de este módulo):
- Aplicarás exactamente este framework: defines tests para una calculator, Claude Code implementa
- Experimentarás la diferencia entre darle instrucciones en lenguaje natural vs darle tests como spec
- Verás cómo Claude Code produce implementaciones más precisas cuando tiene tests como contexto
El patrón spec-first que aprendes aquí se repite en todos los módulos de la guía.
Troubleshooting
Problema 1: "No sé qué tests escribir — no conozco el dominio"
Causa: Confundir "no conozco la implementación" con "no conozco el comportamiento." No necesitas saber cómo se implementa un password checker para saber que "abc" es una password débil.
Solución: Piensa como usuario, no como developer. ¿Qué inputs darías? ¿Qué outputs esperarías? Si aún no estás seguro, empieza con el caso más simple y expande:
# Empieza aquí (el caso más obvio):
def test_basic():
assert add(2, 3) == 5
# Luego expande:
def test_zero():
assert add(0, 5) == 5
# Luego edge cases:
def test_negative():
assert add(-1, 1) == 0
Problema 2: "Mis tests son demasiado específicos — limitan la implementación"
Causa: Testear implementación en vez de comportamiento.
Solución: Testea QUÉ retorna, no CÓMO lo calcula:
# ❌ Demasiado específico (testea implementación):
def test_uses_regex():
import re
assert re.search(r"[A-Z]", "Hello") # Forzas regex
# ✅ Testea comportamiento:
def test_detects_uppercase():
result = check_password_strength("Hello")
assert result["has_uppercase"] == True
Problema 3: "Claude Code no entiende mis tests"
Causa: Tests que dependen de imports o setup que no diste como contexto.
Solución: Incluye todo lo que Claude Code necesita para entender los tests:
- El archivo de tests completo (incluyendo imports)
- El nombre del archivo donde debe implementar
- Cualquier dependencia o constraint ("usa solo la standard library")
Ejercicios
Ejercicio 1: Convertir spec natural a tests (Fácil)
Convierte esta especificación en lenguaje natural a tests:
"Necesito una función celsius_to_fahrenheit que convierta temperatura de Celsius a Fahrenheit. La fórmula es F = C * 9/5 + 32. Debe funcionar con negativos y cero."
Ver solución
import pytest
from temperature import celsius_to_fahrenheit
def test_freezing_point():
assert celsius_to_fahrenheit(0) == 32.0
def test_boiling_point():
assert celsius_to_fahrenheit(100) == 212.0
def test_body_temperature():
assert celsius_to_fahrenheit(37) == 98.6
def test_negative_temperature():
assert celsius_to_fahrenheit(-40) == -40.0
def test_absolute_zero():
assert celsius_to_fahrenheit(-273.15) == pytest.approx(-459.67)
Explicación: Cada test usa valores conocidos (punto de congelación, ebullición, temperatura corporal) para verificar la conversión. pytest.approx maneja imprecisiones de punto flotante. Nota que no necesitas saber la fórmula para escribir estos tests — solo necesitas conocer los puntos de referencia.
Ejercicio 2: Identificar ambigüedad (Fácil)
Lee esta especificación y lista 3 ambigüedades que tests resolverían:
"Implementa una función truncate_text que acorte texto largo. Si el texto es más largo que el límite, córtalo y agrega '...' al final."
Ver solución
Ambigüedades:
- ¿El límite incluye los "..."? Si el límite es 10, ¿el resultado tiene 10 chars (incluyendo "...") o 13 chars (10 + "...")?
- ¿Qué pasa si el texto es exactamente del largo del límite? ¿Se trunca o no?
- ¿Qué pasa con texto más corto que el límite? ¿Se retorna igual?
- ¿Qué pasa con texto vacío? ¿Retorna ""? ¿Retorna "..."?
- ¿Qué pasa con límite de 0 o negativo?
Tests que resolverían las ambigüedades:
def test_long_text_truncated_with_ellipsis():
assert truncate_text("Hello World!", 5) == "Hello..."
def test_text_at_limit_not_truncated():
assert truncate_text("Hello", 5) == "Hello"
def test_short_text_unchanged():
assert truncate_text("Hi", 5) == "Hi"
def test_empty_text():
assert truncate_text("", 5) == ""
def test_zero_limit():
assert truncate_text("Hello", 0) == "..."
Explicación: Cada test resuelve una ambigüedad. Ahora Claude Code sabe exactamente qué comportamiento implementar.
Ejercicio 3: Spec-first para un caso real (Medio)
Necesitas una función parse_duration(text: str) -> int que convierta duración en texto a segundos. Ejemplos: "2h" → 7200, "30m" → 1800, "45s" → 45.
Escribe 6+ tests como spec. No implementes la función.
Ver solución
import pytest
from duration_parser import parse_duration
def test_hours():
assert parse_duration("2h") == 7200
def test_minutes():
assert parse_duration("30m") == 1800
def test_seconds():
assert parse_duration("45s") == 45
def test_one_hour():
assert parse_duration("1h") == 3600
def test_zero():
assert parse_duration("0s") == 0
def test_invalid_unit_raises():
with pytest.raises(ValueError, match="Invalid duration format"):
parse_duration("5x")
def test_no_number_raises():
with pytest.raises(ValueError, match="Invalid duration format"):
parse_duration("h")
def test_empty_string_raises():
with pytest.raises(ValueError, match="Invalid duration format"):
parse_duration("")
def test_negative_raises():
with pytest.raises(ValueError, match="must be non-negative"):
parse_duration("-5m")
Explicación: Estos tests definen completamente el contrato de parse_duration: qué inputs acepta, qué outputs produce, y cómo maneja errores. Claude Code puede implementar esto sin ambigüedad.
Ejercicio 4: Detectar spec incompleta (Medio)
Estos tests son para una función divide(a, b). ¿Qué edge cases faltan?
def test_basic_division():
assert divide(10, 2) == 5.0
def test_division_with_remainder():
assert divide(7, 2) == 3.5
def test_divide_by_zero():
with pytest.raises(ZeroDivisionError):
divide(10, 0)
Ver solución
Edge cases faltantes:
def test_divide_negative_numbers():
assert divide(-10, 2) == -5.0
def test_divide_two_negatives():
assert divide(-10, -2) == 5.0
def test_divide_zero_by_number():
assert divide(0, 5) == 0.0
def test_divide_very_large_numbers():
assert divide(1e15, 1e10) == 1e5
def test_divide_very_small_result():
assert divide(1, 3) == pytest.approx(0.333333, rel=1e-4)
def test_divide_float_inputs():
assert divide(1.5, 0.5) == 3.0
Explicación: La spec original solo cubría el happy path y un error case. Una spec completa incluye negativos, cero como numerador, números grandes, precision de floats, y inputs float. Estos son los edge cases que Claude Code podría no manejar sin tests que los requieran.
Resumen
En esta cápsula aprendiste:
- ✅ La inversión de control en agentic TDD: tú defines specs (tests), Claude Code implementa
- ✅ Tests como especificación: precisos, ejecutables, verificables — superiores al lenguaje natural
- ✅ El framework spec-first en 4 pasos: define tests → da contexto → ejecuta → itera
- ✅ La analogía arquitecto/constructor: tú diseñas los planos, Claude Code construye
- ✅ Spec-first completo en práctica: password checker desde tests hasta implementación validada
- ✅ Combinar lenguaje natural (contexto) con tests (especificación) para mejores resultados
Próxima cápsula: Anatomía de un buen test-spec — qué hace que un test sea una buena especificación.
Recursos Adicionales
- Tweag Blog - Artículos sobre desarrollo con LLMs y metodologías spec-first
- pytest: Writing Tests - Documentación oficial sobre assertions en pytest
- Specification by Example (Gojko Adzic) - El concepto de "especificación mediante ejemplos" aplicado a software
- BDD vs TDD - Comparación entre Behavior-Driven y Test-Driven Development
- Test Desiderata (Kent Beck) - Propiedades que hacen un buen test
Módulo 1, Cápsula 03 — Testing with Claude Code Guide Tú defines el qué, Claude Code implementa el cómo