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ó:

  1. Test falla → pytest da feedback preciso
  2. Das el feedback a Claude Code → Claude Code entiende exactamente el problema
  3. 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

AspectoTest-First (lo que hiciste)Test-After (alternativa)
InicioEscribiste 19 testsPediste implementación
Ambigüedad0 — tests definen comportamientoAlta — "crea string utils"
Edge casesDefinidos ANTES de implementarDescubiertos DESPUÉS (si acaso)
Confianza19 tests pasan = correcto"Se ve bien" = esperanza
RegresionesTests detectan cambiosPrueba manual (si te acuerdas)
Costo de agregar testsYa están escritosHay 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"
  1. Agrega este test a tus tests existentes
  2. Ejecuta pytest — observa el fallo
  3. Da el output del fallo a Claude Code
  4. Claude Code corrige
  5. 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) -> int
  • is_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

  1. pytest: Usage and Invocations - Opciones de línea de comando de pytest (-v, -k, etc.)
  2. pytest: Output Formatting - Cómo leer y configurar el output de pytest
  3. Anthropic: Claude Code Getting Started - Setup y primeros pasos con Claude Code
  4. Real Python: Getting Started with pytest - Tutorial práctico de pytest
  5. 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