Módulo 1: Spec-First Methodology y TDD con AI
Tu Primer Ciclo Spec→Implement con Claude Code
Tu Primer Ciclo Spec→Implement con Claude Code
Descripción de la cápsula
Has aprendido la filosofía (por qué TDD con AI), la metodología (spec-first), y la técnica (anatomía de un buen test-spec). Ahora es momento de ejecutarlo. En esta cápsula vas a completar tu primer ciclo spec→implement→validate con Claude Code — de principio a fin, hands-on.
El ciclo es simple: escribes tests que definen el comportamiento, los das como contexto a Claude Code, Claude Code implementa, y pytest valida. Lo ejecutarás paso a paso con un ejemplo concreto: un módulo de string utilities. Deliberadamente simple para que el foco esté en el workflow, no en la complejidad del dominio.
Al final, habrás experimentado el momento "aha" de spec-first: ver a Claude Code implementar exactamente lo que tus tests definen, sin ambigüedad, sin interpretación. Y habrás vivido el iteration loop: cuando un test falla, Claude Code lee el output de pytest y corrige automáticamente.
Setup: Preparar el Entorno
Antes de empezar, asegúrate de tener el entorno listo.
Instalar pytest
# Crear directorio de trabajo
mkdir spec-first-demo
cd spec-first-demo
# Crear ambiente virtual
python -m venv venv
source venv/bin/activate # Mac/Linux
# venv\Scripts\activate # Windows
# Instalar pytest
pip install pytest
Verificar instalación
pytest --version
# Output esperado (versión puede variar):
pytest 8.x.x
Estructura del proyecto
spec-first-demo/
├── string_utils.py ← Claude Code creará este archivo
├── test_string_utils.py ← Tú crearás este archivo
└── venv/
Paso 1: Escribe los Tests Primero (Tú)
Vas a crear un módulo string_utils.py con 3 funciones. Pero NO vas a implementar las funciones — vas a definir su comportamiento con tests.
Crea el archivo test_string_utils.py:
# test_string_utils.py
import pytest
from string_utils import reverse_string, count_vowels, capitalize_words
# === Tests para reverse_string ===
class TestReverseString:
def test_reverse_simple_word(self):
assert reverse_string("hello") == "olleh"
def test_reverse_sentence(self):
assert reverse_string("hello world") == "dlrow olleh"
def test_reverse_empty_string(self):
assert reverse_string("") == ""
def test_reverse_single_char(self):
assert reverse_string("a") == "a"
def test_reverse_palindrome(self):
assert reverse_string("racecar") == "racecar"
def test_reverse_with_spaces(self):
assert reverse_string(" hi ") == " ih "
# === Tests para count_vowels ===
class TestCountVowels:
def test_count_lowercase_vowels(self):
assert count_vowels("hello") == 2
def test_count_uppercase_vowels(self):
assert count_vowels("HELLO") == 2
def test_count_mixed_case(self):
assert count_vowels("Hello World") == 3
def test_no_vowels(self):
assert count_vowels("rhythm") == 0
def test_all_vowels(self):
assert count_vowels("aeiou") == 5
def test_empty_string(self):
assert count_vowels("") == 0
def test_numbers_and_symbols(self):
assert count_vowels("h3ll0 w0rld!") == 0
# === Tests para capitalize_words ===
class TestCapitalizeWords:
def test_capitalize_simple_sentence(self):
assert capitalize_words("hello world") == "Hello World"
def test_capitalize_single_word(self):
assert capitalize_words("hello") == "Hello"
def test_capitalize_already_capitalized(self):
assert capitalize_words("Hello World") == "Hello World"
def test_capitalize_all_lowercase(self):
assert capitalize_words("the quick brown fox") == "The Quick Brown Fox"
def test_capitalize_empty_string(self):
assert capitalize_words("") == ""
def test_capitalize_mixed_case(self):
assert capitalize_words("hELLO wORLD") == "Hello World"
Observa lo que hiciste:
- ✅ Definiste 3 funciones sin implementarlas
- ✅ Cada test es determinista, independiente, enfocado
- ✅ Los nombres documentan el comportamiento
- ✅ Incluiste edge cases (vacío, un char, palindromo, sin vowels)
- ✅ Los tests son tu especificación completa
Ejecuta los tests (deben fallar)
pytest test_string_utils.py -v
# Output esperado:
ERRORS - ModuleNotFoundError: No module named 'string_utils'
Esto es correcto: string_utils.py no existe todavía. Estás en la fase RED del TDD — los tests fallan porque la implementación no existe.
Paso 2: Da los Tests a Claude Code
Ahora le das tus tests como contexto a Claude Code. Hay varias formas de hacerlo:
Opción A: En Claude Code CLI
# Si estás usando Claude Code en terminal:
claude
# Dentro de Claude Code, dale contexto:
> Lee test_string_utils.py y crea string_utils.py con las funciones
> reverse_string, count_vowels, y capitalize_words para que todos
> los tests pasen.
Opción B: En Cursor con Claude Code
Si estás en Cursor, selecciona test_string_utils.py como contexto y pide:
Crea string_utils.py con las implementaciones de reverse_string,
count_vowels, y capitalize_words. Los tests en test_string_utils.py
definen el comportamiento esperado — implementa para que todos pasen.
Qué le estás comunicando a Claude Code
Al darle los tests, Claude Code recibe:
De tus tests, Claude Code infiere:
reverse_string(str) -> str
- Invierte el string completo (no las palabras)
- Funciona con strings vacíos
- Preserva espacios en la posición invertida
count_vowels(str) -> int
- Cuenta a, e, i, o, u (case-insensitive)
- Retorna 0 para strings sin vowels
- Números y símbolos no son vowels
capitalize_words(str) -> str
- Primera letra de cada palabra en mayúscula
- Resto de cada palabra en minúscula
- Funciona con strings vacíos
No tuviste que escribir esta especificación en lenguaje natural — los tests la comunican implícitamente.
Paso 3: Claude Code Implementa
Claude Code genera algo similar a esto:
# string_utils.py
def reverse_string(text: str) -> str:
"""Reverse the entire string."""
return text[::-1]
def count_vowels(text: str) -> int:
"""Count vowels (a, e, i, o, u) in text, case-insensitive."""
return sum(1 for char in text.lower() if char in "aeiou")
def capitalize_words(text: str) -> str:
"""Capitalize first letter of each word, lowercase the rest."""
return " ".join(word.capitalize() for word in text.split(" ")) if text else ""
Observa:
- Claude Code dedujo la firma de las funciones desde los tests
- La implementación es limpia y directa
- No necesitaste explicar la lógica — los tests la definieron
Paso 4: Ejecuta pytest (Validación)
pytest test_string_utils.py -v
test_string_utils.py::TestReverseString::test_reverse_simple_word PASSED
test_string_utils.py::TestReverseString::test_reverse_sentence PASSED
test_string_utils.py::TestReverseString::test_reverse_empty_string PASSED
test_string_utils.py::TestReverseString::test_reverse_single_char PASSED
test_string_utils.py::TestReverseString::test_reverse_palindrome PASSED
test_string_utils.py::TestReverseString::test_reverse_with_spaces PASSED
test_string_utils.py::TestCountVowels::test_count_lowercase_vowels PASSED
test_string_utils.py::TestCountVowels::test_count_uppercase_vowels PASSED
test_string_utils.py::TestCountVowels::test_count_mixed_case PASSED
test_string_utils.py::TestCountVowels::test_no_vowels PASSED
test_string_utils.py::TestCountVowels::test_all_vowels PASSED
test_string_utils.py::TestCountVowels::test_empty_string PASSED
test_string_utils.py::TestCountVowels::test_numbers_and_symbols PASSED
test_string_utils.py::TestCapitalizeWords::test_capitalize_simple_sentence PASSED
test_string_utils.py::TestCapitalizeWords::test_capitalize_single_word PASSED
test_string_utils.py::TestCapitalizeWords::test_capitalize_already_capitalized PASSED
test_string_utils.py::TestCapitalizeWords::test_capitalize_all_lowercase PASSED
test_string_utils.py::TestCapitalizeWords::test_capitalize_empty_string PASSED
test_string_utils.py::TestCapitalizeWords::test_capitalize_mixed_case PASSED
========================= 19 passed in 0.02s =========================
19/19 tests pasan. La implementación cumple exactamente tu especificación. Estás en la fase GREEN del TDD.
Paso 5: ¿Y Cuando un Test Falla? (Iteration Loop)
El ciclo perfecto donde todo pasa al primer intento es común con funciones simples. Pero en código real, a veces Claude Code genera una implementación que no pasa todos los tests. Veamos cómo manejar eso.
Simulemos un fallo
Imagina que agregas un test más exigente:
# Agrega este test a TestCapitalizeWords:
def test_capitalize_preserves_multiple_spaces(self):
assert capitalize_words("hello world") == "Hello World"
Ejecutas pytest:
pytest test_string_utils.py::TestCapitalizeWords::test_capitalize_preserves_multiple_spaces -v
FAILED test_string_utils.py::TestCapitalizeWords::test_capitalize_preserves_multiple_spaces
AssertionError: assert 'Hello World' == 'Hello World'
La implementación de Claude Code usó text.split(" ") que colapsa múltiples espacios en uno. Tu test define que los espacios múltiples se deben preservar.
El iteration loop
Le das el output de pytest a Claude Code:
El test test_capitalize_preserves_multiple_spaces falla:
AssertionError: assert 'Hello World' == 'Hello World'
La función capitalize_words está colapsando múltiples espacios.
Corrige para preservar espacios múltiples.
Claude Code corrige:
import re
def capitalize_words(text: str) -> str:
"""Capitalize first letter of each word, lowercase the rest."""
if not text:
return ""
return re.sub(
r'\S+',
lambda m: m.group().capitalize(),
text
)
Ejecutas pytest de nuevo:
pytest test_string_utils.py -v
========================= 20 passed in 0.02s =========================
El iteration loop funcionó:
- Test falla → pytest da feedback preciso
- Das el feedback a Claude Code → Claude Code entiende exactamente el problema
- Claude Code corrige → pytest valida la corrección
No tuviste que explicar el bug en lenguaje natural. El output de pytest fue suficiente.
El Contra-Ejemplo: Sin Tests
Para entender el valor del ciclo spec-first, compara con el workflow sin tests.
Sin tests: Lo que habrías hecho
1. "Claude, crea un módulo string_utils con reverse_string,
count_vowels, y capitalize_words"
2. Claude genera implementación
3. Tú pruebas manualmente:
>>> reverse_string("hello")
'olleh' # ✅ "Se ve bien"
>>> count_vowels("hello")
2 # ✅ "Correcto"
>>> capitalize_words("hello world")
'Hello World' # ✅ "Funciona"
4. "Todo funciona" → push
5. Dos semanas después:
capitalize_words("hello world") → "Hello World" # ❌ Bug!
count_vowels("café") → 2 # ❌ ¿Y la é?
Con tests: Lo que hiciste
1. Escribiste 19 tests que definen TODO el comportamiento
2. Claude Code implementó para que los tests pasen
3. pytest validó en 0.02 segundos
4. Cuando encontraste un caso nuevo (espacios múltiples),
agregaste un test, Claude Code corrigió, pytest validó
5. Tienes 20 tests como red de seguridad permanente
→ Si alguien cambia string_utils.py, los tests detectan regresiones
Comparación: Test-First vs Test-After en Este Ejemplo
| Aspecto | Test-First (lo que hiciste) | Test-After (alternativa) |
|---|---|---|
| Inicio | Escribiste 19 tests | Pediste implementación |
| Ambigüedad | 0 — tests definen comportamiento | Alta — "crea string utils" |
| Edge cases | Definidos ANTES de implementar | Descubiertos DESPUÉS (si acaso) |
| Confianza | 19 tests pasan = correcto | "Se ve bien" = esperanza |
| Regresiones | Tests detectan cambios | Prueba manual (si te acuerdas) |
| Costo de agregar tests | Ya están escritos | Hay que escribirlos después |
El costo de test-first: ~15 minutos más para escribir los tests antes de implementar.
El beneficio de test-first: Validación automática permanente. Si en 3 meses alguien (o Claude Code) cambia string_utils.py, los 20 tests verifican instantáneamente que nada se rompió.
Conexión con Proyecto
En el Spec-first mini-app (proyecto de este módulo):
- Aplicarás exactamente este ciclo a una calculator: escribes tests → Claude Code implementa → pytest valida
- Experimentarás iteration loops reales cuando Claude Code no cubra todos los edge cases al primer intento
- Practicarás dar feedback de pytest a Claude Code para correcciones
Este es el workflow que usarás en toda la guía y en todo tu desarrollo futuro con Claude Code.
Troubleshooting
Problema 1: "Claude Code generó la implementación pero los imports fallan"
Causa: El archivo de implementación tiene un nombre diferente al que esperas los tests, o está en un directorio diferente.
Solución: Verifica que string_utils.py esté en el mismo directorio que test_string_utils.py. Los tests hacen from string_utils import ... — el archivo debe llamarse exactamente string_utils.py.
ls -la *.py
# Debe mostrar:
# string_utils.py
# test_string_utils.py
Problema 2: "pytest no encuentra los tests"
Causa: Los tests no siguen la convención de naming de pytest.
Solución: Verifica que:
- El archivo empieza con
test_(ej:test_string_utils.py) - Las funciones de test empiezan con
test_(ej:def test_reverse_simple_word) - Las clases empiezan con
Test(ej:class TestReverseString)
# Ejecuta con verbose para ver qué descubre pytest:
pytest test_string_utils.py -v --collect-only
Problema 3: "Claude Code no implementa lo que mis tests esperan"
Causa: Los tests pueden no dar suficiente contexto. Claude Code infiere la firma y comportamiento de los tests, pero a veces la inferencia es incorrecta.
Solución: Agrega contexto en lenguaje natural junto con los tests:
"Implementa string_utils.py con estas funciones:
- reverse_string: invierte todo el string (no las palabras individuales)
- count_vowels: cuenta a, e, i, o, u (case-insensitive)
- capitalize_words: primera letra de cada palabra en mayúscula, resto en minúscula
Los tests en test_string_utils.py definen el comportamiento exacto."
Problema 4: "El iteration loop se siente lento"
Causa: Estás copiando manualmente el output de pytest y pegándolo en Claude Code.
Solución: En Claude Code CLI, puedes ejecutar pytest directamente y Claude Code ve el output. En módulos posteriores (especialmente módulo 6) aprenderás sobre validation loops automáticos donde Claude Code ejecuta, lee el output, y corrige sin intervención manual.
Ejercicios
Ejercicio 1: Tu propio ciclo spec-first (Fácil)
Escribe 4 tests para una función is_palindrome(text: str) -> bool y luego pide a Claude Code que la implemente. Ejecuta pytest para validar.
Ver solución
# test_palindrome.py
from palindrome import is_palindrome
def test_palindrome_simple():
assert is_palindrome("racecar") == True
def test_not_palindrome():
assert is_palindrome("hello") == False
def test_palindrome_case_insensitive():
assert is_palindrome("Racecar") == True
def test_empty_string_is_palindrome():
assert is_palindrome("") == True
Implementación esperada de Claude Code:
# palindrome.py
def is_palindrome(text: str) -> bool:
cleaned = text.lower()
return cleaned == cleaned[::-1]
pytest test_palindrome.py -v
# 4 passed
Explicación: Los tests definen que is_palindrome debe ser case-insensitive y que un string vacío es palindromo. Claude Code infiere ambos requisitos de los tests.
Ejercicio 2: Agregar edge cases (Fácil)
Toma los tests del Ejercicio 1 y agrega 3 edge cases más. Ejecuta pytest — ¿pasan con la implementación existente?
Ver solución
# Tests adicionales:
def test_palindrome_single_char():
assert is_palindrome("a") == True
def test_palindrome_with_spaces():
assert is_palindrome("race car") == False # Espacios cuentan
def test_palindrome_numbers():
assert is_palindrome("12321") == True
pytest test_palindrome.py -v
# 7 passed (si la implementación original es correcta)
Explicación: test_palindrome_with_spaces define que los espacios SÍ cuentan (no se ignoran). Si quisieras que "race car" sea palindromo (ignorando espacios), el test sería assert is_palindrome("race car") == True — y Claude Code implementaría la limpieza de espacios.
Ejercicio 3: Experimentar el iteration loop (Medio)
Escribe un test que SABES que fallará con una implementación básica:
def test_capitalize_words_with_apostrophes():
assert capitalize_words("it's a beautiful day") == "It's A Beautiful Day"
- Agrega este test a tus tests existentes
- Ejecuta pytest — observa el fallo
- Da el output del fallo a Claude Code
- Claude Code corrige
- Ejecuta pytest de nuevo
Documenta cada paso del iteration loop.
Ver solución
Paso 1: Agregar test
Paso 2: pytest falla:
FAILED test_string_utils.py::TestCapitalizeWords::test_capitalize_words_with_apostrophes
AssertionError: assert "It'S A Beautiful Day" == "It's A Beautiful Day"
El problema: .capitalize() en Python convierte la primera letra a mayúscula y el RESTO a minúscula — incluyendo la s después del apóstrofo. Pero el test espera que it's → It's (no It'S).
Paso 3: Le das a Claude Code:
El test test_capitalize_words_with_apostrophes falla:
"It'S A Beautiful Day" != "It's A Beautiful Day"
.capitalize() convierte it's → It'S. Necesito que solo la primera
letra de la palabra sea mayúscula, sin afectar las demás.
Paso 4: Claude Code corrige:
def capitalize_words(text: str) -> str:
if not text:
return ""
def capitalize_first(word):
if not word:
return word
return word[0].upper() + word[1:].lower()
return re.sub(r'\S+', lambda m: capitalize_first(m.group()), text)
Hmm, pero esto convierte it's → It's (correcto) pero también hELLO → Hello (que es lo que queremos según test_capitalize_mixed_case).
Paso 5: pytest pasa todos los tests.
Lección del iteration loop: El feedback de pytest fue suficiente para que Claude Code entendiera y corrigiera el problema. No necesitaste explicar la mecánica de .capitalize() — el output del test lo hizo por ti.
Ejercicio 4: Spec-first para math utils (Medio)
Crea una spec completa (8+ tests) para un módulo math_utils.py con estas funciones:
factorial(n: int) -> intis_prime(n: int) -> bool
Escribe SOLO los tests. No implementes las funciones.
Ver solución
# test_math_utils.py
import pytest
from math_utils import factorial, is_prime
class TestFactorial:
def test_factorial_zero(self):
assert factorial(0) == 1
def test_factorial_one(self):
assert factorial(1) == 1
def test_factorial_five(self):
assert factorial(5) == 120
def test_factorial_ten(self):
assert factorial(10) == 3628800
def test_factorial_negative_raises(self):
with pytest.raises(ValueError, match="must be non-negative"):
factorial(-1)
class TestIsPrime:
def test_two_is_prime(self):
assert is_prime(2) == True
def test_three_is_prime(self):
assert is_prime(3) == True
def test_four_is_not_prime(self):
assert is_prime(4) == False
def test_one_is_not_prime(self):
assert is_prime(1) == False
def test_zero_is_not_prime(self):
assert is_prime(0) == False
def test_large_prime(self):
assert is_prime(97) == True
def test_negative_raises(self):
with pytest.raises(ValueError, match="must be non-negative"):
is_prime(-5)
Explicación: 12 tests que definen completamente factorial e is_prime:
- Happy path (valores conocidos)
- Boundary values (0, 1, 2)
- Edge cases (negativos → ValueError)
- Caso especial (1 no es primo)
Darle estos tests a Claude Code producirá una implementación correcta y completa.
Resumen
En esta cápsula aprendiste:
- ✅ El ciclo spec-first completo: escribir tests → dar a Claude Code → ejecutar pytest → iterar si falla
- ✅ Setup práctico: crear proyecto, instalar pytest, estructura de archivos
- ✅ Cómo dar tests como contexto a Claude Code (CLI o Cursor)
- ✅ El iteration loop: pytest falla → das output a Claude Code → Claude corrige → pytest valida
- ✅ Contraste con el workflow sin tests: menos confianza, más bugs, sin red de seguridad
- ✅ Test-first cuesta ~15 minutos más pero da validación automática permanente
Próxima cápsula: Proyecto — Spec-first mini-app. Aplicarás todo lo aprendido construyendo una calculator completa con spec-first methodology.
Recursos Adicionales
- pytest: Usage and Invocations - Opciones de línea de comando de pytest (-v, -k, etc.)
- pytest: Output Formatting - Cómo leer y configurar el output de pytest
- Anthropic: Claude Code Getting Started - Setup y primeros pasos con Claude Code
- Real Python: Getting Started with pytest - Tutorial práctico de pytest
- TDD by Example (Kent Beck) - Referencia clásica para entender el ciclo red-green-refactor
Módulo 1, Cápsula 05 — Testing with Claude Code Guide De spec a implementación en minutos — tu primer ciclo completo