Módulo 4: TDD Workflow Completo
El Ciclo de Implementación con Claude Code
El Ciclo de Implementación con Claude Code
Descripción de la cápsula
Tienes un test rojo. Le pides a Claude Code que implemente. ¿Qué contexto necesita? ¿Qué haces cuando falla? ¿Cuándo ajustas el prompt y cuándo ajustas el test? Esta cápsula responde esas preguntas con un framework concreto. Es la más importante del módulo: cubre la interacción entre tú y Claude Code durante la fase verde del TDD.
En TDD clásico, tú escribes el test Y la implementación. Con Claude Code, escribes el test y la AI implementa — pero a veces la implementación falla. A veces pasa el test pero el código es malo. A veces Claude Code sobre-ingenieriza o hace trampa. Esta cápsula te da el juicio para manejarlo.
Al final entenderás cómo dar contexto efectivo, evaluar implementaciones, y decidir cuándo intervenir vs dejar que Claude Code itere.
El Prompt de Implementación
Qué contexto necesita Claude Code
Claude Code no tiene acceso a tu intención ni a las convenciones del proyecto. Lo que recibe es lo que le das. Para implementar correctamente, necesita tres tipos de información:
1. El archivo de tests
Sin el test, Claude Code está adivinando qué debe hacer. Con el test, tiene una especificación ejecutable.
2. Interfaces existentes
Si la función será llamada por un endpoint FastAPI, o si debe encajar en una clase existente, Claude Code necesita ver esa interfaz.
3. Constraints explícitos
"Sin librerías externas", "debe ser async", "usa solo la stdlib", "compatible con Python 3.10". Sin constraints, Claude Code puede elegir dependencias o enfoques que no quieres.
Patrón de prompt efectivo
Implementa [función/clase] para que pase estos tests: [tests].
Constraints: [restricciones].
Contexto: [interfaces o código existente].
Este patrón es genérico. Lo importante es que los tests, constraints e interfaces estén presentes de forma explícita.
Buen prompt vs mal prompt
| Aspecto | Mal prompt | Buen prompt |
|---|---|---|
| Especificación | "Implementa un token generator" | "Implementa generate_token(user_id: str) -> str que pase estos tests: [código del test]" |
| Tests | No incluye tests | Incluye el contenido del archivo test_*.py |
| Constraints | Ninguno | "Sin librerías externos, solo stdlib, longitud mínima 32 caracteres" |
| Interfaces | Vago | "Esta función será llamada desde auth/login.py que espera un str" |
| Resultado típico | Claude adivina, puede sobre-ingenierizar | Claude tiene specs claras, implementación acotada |
Ejemplo de mal prompt:
Implementa un sistema de tokens para autenticación.
Por qué falla: Claude Code podría usar PyJWT, generar tokens con formato propio, usar secrets o uuid, incluir claims que no necesitas. No hay forma de validar si la implementación es correcta más allá de "se ve bien".
Ejemplo de buen prompt:
Implementa la función generate_token(user_id: str) -> str en auth/tokens.py
para que pase estos tests (están en tests/test_tokens.py):
def test_generate_token_returns_string():
token = generate_token("user-123")
assert isinstance(token, str)
assert len(token) >= 32
def test_generate_token_different_each_time():
t1 = generate_token("user-123")
t2 = generate_token("user-123")
assert t1 != t2
Constraints:
- Solo stdlib de Python (no PyJWT ni otras dependencias)
- La función debe ser pura (mismo input no garantiza mismo output por seguridad)
Por qué funciona: Tests ejecutables, constraints claros, archivo destino explícito. Claude Code sabe exactamente qué implementar y qué no.
Evaluar la Implementación de Claude Code
Correr los tests no basta. Necesitas tres chequeos:
1. ¿Pasan los tests?
pytest tests/test_tokens.py -v
Si fallan, entra el framework de decisión (siguiente sección). Si pasan, continúa.
2. ¿El código es correcto Y bueno?
Pasar tests no garantiza buena implementación. Revisa:
- Lógica: ¿Resuelve el problema de forma genuina o hace trampa?
- Edge cases: ¿Maneja entradas vacías, None, strings muy largos?
- Estilo: ¿Sigue las convenciones del proyecto?
3. Señales de alerta
Over-engineering: Claude Code a veces agrega features que los tests no piden.
# Test pide: retornar un string de al menos 32 caracteres
# Claude implementa: clase TokenManager con refresh, expire, claims, etc.
Si el test solo pide una función simple, la implementación debe ser simple.
Shortcuts / hardcoding: Claude Code a veces hace trampa para pasar el test.
# Test: assert generate_token("user-123") != generate_token("user-123")
# Claude implementa:
def generate_token(user_id: str) -> str:
return str(uuid.uuid4()) # OK, pasa el test
# Pero otro test: assert "user-123" in generate_token("user-123")
# Claude podría hacer trampa:
def generate_token(user_id: str) -> str:
return f"user-123-{uuid.uuid4()}" # Hardcodea user_id en el output
La segunda implementación pasa un test específico pero no cumple el requisito real (token que contenga el user_id de forma usable para validación). Detectar estos shortcuts requiere leer el código con ojo crítico.
Test que detecta hardcoding: Usa @pytest.mark.parametrize con varios inputs. Si Claude hardcodea para un solo caso, fallará con otros:
@pytest.mark.parametrize("user_id", ["user-1", "user-2", "admin-99"])
def test_token_contains_user_id(user_id):
token = generate_token(user_id)
assert user_id in token
Un return "user-123-abc" hardcodeado fallaría para "user-2" y "admin-99".
Cuando Claude Code Falla los Tests: Framework de Decisión
Esta es la habilidad clave del módulo. Cuando un test falla después de que Claude Code implementa, ¿qué haces?
Test falla después de que Claude implementa
│
├── ¿El test es correcto?
│ │
│ ├── SÍ → Problema de implementación
│ │ ├── Falta de contexto → Agrega más contexto al prompt
│ │ ├── Enfoque equivocado → Sugiere otro enfoque
│ │ └── Edge case → Deja que Claude Code itere
│ │
│ └── NO → Problema de spec
│ ├── Test ambiguo → Clarifica el test
│ ├── Test sobre-especificado → Simplifica el test
│ └── Falta de constraint → Agrega el constraint
Pregunta guía: "¿Qué estoy tratando de especificar?"
- Si el test captura correctamente tu intención → el problema está en la implementación. Da más contexto, sugiere enfoque, o deja iterar.
- Si al leer el test te das cuenta de que no expresa bien lo que quieres → el problema está en la spec. Ajusta el test.
Ejemplos de aplicación
Caso 1: Falta de contexto
Test falla: generate_token("") debe lanzar ValueError, pero Claude implementó una función que acepta strings vacíos.
Diagnóstico: El test es correcto (no queremos tokens para user_id vacío). Claude no tenía ese requisito explícito.
Acción: Agregar al prompt: "Debe lanzar ValueError si user_id es vacío o solo espacios."
Caso 2: Test ambiguo
Test: assert len(generate_token("user-1")) >= 10. Claude retorna un string de 10 caracteres que es siempre el mismo para "user-1".
Diagnóstico: El test no especificaba que debe ser aleatorio o único. La implementación "pasa" pero no cumple el requisito real.
Acción: Clarificar el test: agregar test_generate_token_different_each_time o hacer el assert más específico.
Caso 3: Dejar iterar
Test falla con AssertionError: assert 31 >= 32. Claude generó tokens de 31 caracteres.
Diagnóstico: Test correcto, implementación casi correcta. Error menor.
Acción: "El test falla porque el token tiene 31 caracteres. Debe tener al menos 32." Claude Code suele corregir esto en una iteración.
Caso 4: Test sobre-especificado
Test: assert generate_token("x")[:1] == "x". Fallas porque el token tiene formato user_id:nonce:signature y para user_id "x" el token podría ser x:abc:def, así que [:1] da "x". Pero si el formato es nonce:user_id:sign, entonces [:1] podría ser "a" (del nonce).
Diagnóstico: El test asume un orden concreto de los campos. Es frágil y sobre-especificado.
Acción: Reescribir el test para verificar comportamiento, no formato interno: assert "x" in generate_token("x") en vez de asumir la posición exacta.
Mensajes de iteración efectivos
Cuando delegas a Claude Code la corrección, el mensaje importa:
| Mensaje vago | Mensaje efectivo |
|---|---|
| "No funciona" | "El test X falla con AssertionError: [pegar output]. El valor esperado es Y pero obtuviste Z." |
| "Arregla el token" | "generate_token retorna 31 caracteres. El test exige >= 32. Cambia la longitud del nonce o del signature." |
| "Hay un bug" | "test_parse_config_empty_raises falla: la función no lanza ValueError para string vacío. Agrega esa validación." |
Incluir el output exacto de pytest (traceback y valores) acelera las iteraciones.
Dar Contexto Efectivo
Incluir test Y código existente
No des solo el test en el prompt. Si hay módulos que llamarán a esta función, inclúyelos. Así Claude Code ve la firma esperada y el flujo.
# auth/login.py (existente)
from auth.tokens import generate_token
def login(user_id: str, password: str) -> dict:
# ... validación ...
token = generate_token(user_id) # <- Claude ve cómo se usa
return {"token": token, "user_id": user_id}
Especificar constraints
Ejemplos útiles:
- "No uses librerías externas"
- "Debe ser async"
- "Compatible con Python 3.10"
- "Máximo 50 líneas"
- "Implementación mínima que pase los tests"
Especificar interfaces
"Esta función será llamada desde el endpoint POST /login que espera un token en formato JWT-like (header.payload.signature)".
Ejemplo: mismo prompt con poco vs mucho contexto
Poco contexto:
Implementa generate_token para que pase test_tokens.py
Mucho contexto:
Implementa generate_token(user_id: str) -> str en auth/tokens.py.
Archivo de tests (tests/test_tokens.py):
[paste completo del archivo]
El endpoint POST /login en auth/routes.py llama a esta función y retorna
el token al cliente. El token debe ser un string que permita identificar
al usuario en requests subsiguientes.
Constraints:
- Solo stdlib de Python
- No usar PyJWT (lo agregaremos después)
- user_id vacío o None debe lanzar ValueError
- Longitud mínima 32 caracteres para seguridad básica
El segundo prompt produce implementaciones más acotadas y correctas.
Checklist antes de enviar el prompt
Antes de pedir implementación, verifica:
- ✅ Incluiste el contenido completo del archivo de tests (o la ruta si Claude tiene acceso al repo)
- ✅ Especificaste el archivo o módulo destino donde debe ir el código
- ✅ Listaste los constraints (librerías, estilo, límites)
- ✅ Incluiste código existente relevante (llamadas, interfaces, imports)
- ✅ El test está en rojo (falla) antes de pedir implementación
Si falta algo de esto, la probabilidad de que Claude Code acierte a la primera disminuye.
El Loop de Iteración
Escribir test → Prompt a Claude → ¿Fallan tests?
│
├── SÍ → Evaluar por qué
│ ├── Agregar contexto → Re-prompt
│ ├── Corregir test → Re-ejecutar
│ └── Dejar iterar → "Corrige el test que falla: [mensaje de error]"
│
└── NO → Todo verde → ¿Refactorizar? → Siguiente test
El ciclo es corto: minutos por iteración. Si llevas 15 minutos en el mismo test, algo está mal: o el prompt es insuficiente, o el test es demasiado complejo.
Señales de que debes cambiar de estrategia
- Más de 3 iteraciones sin éxito: El prompt no está funcionando. Prueba dividir el test en partes más pequeñas o dar un ejemplo de implementación esperada.
- Claude responde con "no puedo" o evasivas: Puede faltar contexto (versión de Python, estructura del proyecto) o el problema puede ser ambiguo. Simplifica el test.
- Implementaciones contradictorias entre iteraciones: Claude "olvida" lo anterior. Incluye en cada iteración un resumen: "La función actual hace X. El problema es Y. Necesito que haga Z."
Ejemplo Real: Construyendo un Token Generator
Vamos a construir generate_token paso a paso, simulando el ciclo con Claude Code.
Paso 1: Primer test
# tests/test_tokens.py
import pytest
from auth.tokens import generate_token
def test_generate_token_returns_string():
"""El token debe ser un string no vacío."""
token = generate_token("user-123")
assert isinstance(token, str)
assert len(token) >= 32
Prompt a Claude Code:
Implementa generate_token(user_id: str) -> str en auth/tokens.py.
Debe pasar el test en tests/test_tokens.py. Solo stdlib.
Paso 2: Claude implementa (posible respuesta)
# auth/tokens.py
import uuid
import hashlib
def generate_token(user_id: str) -> str:
"""Generate a unique token for the given user."""
data = f"{user_id}-{uuid.uuid4()}"
return hashlib.sha256(data.encode()).hexdigest()
Corres pytest tests/test_tokens.py -v → Pasa. Revisas el código: usa stdlib, genera 64 caracteres (sha256 hex). Correcto y adecuado.
Paso 3: Segundo test
def test_token_contains_user_id():
"""El token debe contener el user_id para validación posterior."""
token = generate_token("user-456")
assert "user-456" in token
Corres tests → Falla. El hash SHA256 no incluye el user_id en texto plano. El token es a3f2b1c... y no contiene "user-456".
Paso 4: Iteración
Tienes dos opciones:
A) El test está mal: Si el diseño real es que el token sea opaco (solo el hash) y el user_id se guarde en otra parte (DB, session), entonces el test sobre-especifica. Deberías eliminarlo o cambiarlo a algo como test_token_different_for_different_users.
B) El test está bien: Quieres que el token sea "self-contained" y lleve el user_id de forma legible (por ejemplo, user-456:signature). Entonces el problema es la implementación.
Asumamos que elegiste B. Tu prompt de iteración:
El test test_token_contains_user_id falla. El token debe contener el user_id
en formato legible (ej: "user-456:signature") para que el backend pueda
extraerlo sin consultar la DB. Corrige la implementación.
Claude Code podría responder:
# auth/tokens.py
import uuid
import hashlib
import base64
def generate_token(user_id: str) -> str:
"""Generate a token containing user_id and a signature."""
if not user_id or not user_id.strip():
raise ValueError("user_id cannot be empty")
nonce = uuid.uuid4().hex
payload = f"{user_id}:{nonce}"
signature = hashlib.sha256(payload.encode()).hexdigest()[:32]
token = f"{user_id}:{signature}"
return base64.b64encode(token.encode()).decode()
Corres tests. ¿Pasa test_token_contains_user_id? Depende: si el token es dXNlci00NTY6YWJjZGVm... (base64), "user-456" no está en texto plano en el string. Necesitarías decodificar o ajustar el test.
Para simplificar, un diseño alternativo:
# auth/tokens.py
import uuid
import hashlib
def generate_token(user_id: str) -> str:
"""Generate a token with user_id prefix for validation."""
if not user_id or not user_id.strip():
raise ValueError("user_id cannot be empty")
nonce = uuid.uuid4().hex
signature = hashlib.sha256(f"{user_id}:{nonce}".encode()).hexdigest()[:24]
return f"{user_id}:{nonce}:{signature}"
Aquí el token es user-456:abc123def:xyz789... y "user-456" está presente. Pasa ambos tests.
Paso 5: Test para ValueError
def test_generate_token_empty_user_raises():
"""user_id vacío debe lanzar ValueError."""
with pytest.raises(ValueError, match="cannot be empty"):
generate_token("")
def test_generate_token_none_raises():
"""None debe lanzar ValueError."""
with pytest.raises(ValueError):
generate_token(None) # type: ignore
Si Claude no había manejado estos casos, fallan. El prompt de iteración: "Agrega validación: user_id vacío o None debe lanzar ValueError."
La evolución del código a través de los ciclos es incremental: cada test fuerza un comportamiento nuevo sin romper los anteriores.
Resumen del ejemplo
| Ciclo | Test | Resultado Claude | Acción |
|---|---|---|---|
| 1 | test_generate_token_returns_string | OK (sha256 hex) | Verde, siguiente |
| 2 | test_token_contains_user_id | Falla (hash opaco) | Iterar: agregar user_id al formato |
| 3 | test_generate_token_empty_user_raises | Falla (no validaba) | Iterar: validar y lanzar ValueError |
| 4 | test_generate_token_none_raises | Puede fallar | Iterar o agregar type check |
Cada iteración es un mini-ciclo TDD: test → implementación → validación → corrección si hace falta.
Heurísticas para Cuándo Intervenir
| Situación | Acción |
|---|---|
| Claude falla el mismo test dos veces con el mismo error | Cambia tu prompt. Agrega contexto, constraints, o un ejemplo de output esperado. |
| Claude pasa el test pero el código es malo | Agrega tests más específicos. Por ejemplo, @pytest.mark.parametrize con varios inputs para evitar hardcoding. |
| Claude sobre-ingenieriza | Agrega constraint: "Implementación mínima que pase los tests. No agregar features no pedidas." |
| Claude hardcodea para pasar | Usa @pytest.mark.parametrize con múltiples inputs. Un solo caso permite trampa; varios la hacen evidente. |
| Claude itera pero no converge | Reduce el scope. Si el test es muy grande, divídelo en tests más pequeños. |
| Dudas sobre si el test es correcto | Pregúntate: "¿Este test refleja lo que realmente quiero?" Si no, ajusta el test primero. |
Práctica: Ciclo Completo
Copia este esqueleto y completa el ciclo con Claude Code (o simula el proceso a mano):
# tests/test_slugify.py
import pytest
from utils.text import slugify
def test_slugify_lowercases():
assert slugify("Hello World") == "hello-world"
def test_slugify_replaces_spaces_with_hyphens():
assert slugify("a b c") == "a-b-c"
def test_slugify_removes_special_chars():
assert slugify("hello!!!world") == "helloworld"
def test_slugify_empty_returns_empty():
assert slugify("") == ""
Crea utils/text.py vacío (o con def slugify(s: str) -> str: raise NotImplementedError). Escribe el primer test, pide a Claude Code que implemente, y ve iterando con el framework de decisión cuando algo falle.
Implementación de referencia para slugify
Si prefieres implementar a mano para practicar el pensamiento TDD, aquí tienes una solución mínima que pasa los tests:
# utils/text.py
import re
import unicodedata
def slugify(s: str) -> str:
"""Convert string to URL-friendly slug."""
if not s:
return ""
# Normalizar acentos (é -> e)
s = unicodedata.normalize("NFD", s)
s = "".join(c for c in s if unicodedata.category(c) != "Mn")
# Lowercase, reemplazar espacios y caracteres no alfanuméricos
s = s.lower().strip()
s = re.sub(r"[^a-z0-9\s-]", "", s)
s = re.sub(r"[-\s]+", "-", s)
return s.strip("-")
Esta implementación pasa los cuatro tests del ejemplo. Úsala como referencia para comparar con lo que genera Claude Code.
Troubleshooting
Problema 1: Claude Code ignora mis constraints
Causa: Los constraints están enterrados en texto largo o son poco visibles.
Solución: Pónlos al final del prompt, en lista numerada o con "Constraints:" como encabezado. Repite los más críticos. Si Claude sigue ignorándolos, escribe: "IMPORTANTE: No usar [X]. Solo [Y]."
Problema 2: La implementación pasa los tests pero rompe otro código
Causa: Claude Code no vio el código que llama a esta función. La firma o el contrato no coinciden.
Solución: Incluye en el prompt los archivos que usan la función. Ejemplo: "Esta función es llamada desde auth/login.py que hace token = generate_token(user.id) y espera un str."
Problema 3: Claude Code itera pero no arregla el error
Causa: El mensaje de error no es suficientemente claro, o el test es demasiado complejo.
Solución: Copia el output exacto de pytest en el prompt. Si el test verifica muchas cosas, divídelo en tests más pequeños y pide que implemente uno a uno.
Problema 4: No sé si el problema es el test o la implementación
Causa: El test es ambiguo o expresa mal la intención.
Solución: Usa la pregunta guía: "¿Este test especifica correctamente lo que quiero?" Si no estás seguro, escribe un test más simple que sí exprese la regla. Luego refina.
Problema 5: Claude genera 200 líneas para un test simple
Causa: Sin constraints de minimalidad, Claude Code tiende a ser exhaustivo.
Solución: Agrega "Implementación mínima. Solo el código necesario para pasar los tests. Sin clases extra ni helpers que no se usen."
Conexión con Proyecto
Cada ciclo del proyecto de autenticación (registro, login, tokens, validación) sigue este flujo:
- Escribes un test rojo (por ejemplo,
test_register_hashes_password) - Das a Claude Code el test, constraints e interfaces
- Claude implementa
- Corres tests
- Si fallan, aplicas el framework de decisión (¿test correcto? ¿implementación?)
- Iteras hasta verde
- Refactorizas si hace falta
- Pasas al siguiente test
El proyecto no es solo código final: es practicar este ciclo muchas veces hasta que el juicio "¿intervengo o dejo iterar?" sea automático.
Ejercicios
Ejercicio 1: Identificar tipo de error (Fácil)
Claude Code implementa una función y el test falla con:
AssertionError: assert 'user-123' in 'a1b2c3d4e5f6...'
Según el framework de decisión, ¿podría ser un problema de test o de implementación? ¿Qué harías primero?
Ver solución
Podría ser ambos:
- Problema de implementación: Si el test exige que el user_id esté en el token (por diseño) y Claude generó un hash opaco, la implementación es la que falla. Acción: dar más contexto sobre el formato esperado.
- Problema de test: Si el diseño real es token opaco + user_id en DB, el test está sobre-especificando. Acción: cambiar el test para no exigir que el user_id esté en el string del token.
Primer paso: Leer el test y preguntar "¿Este assert refleja el diseño que quiero?" Si sí, es problema de implementación. Si no, es problema de spec.
Ejercicio 2: Escribir un buen prompt (Medio)
Tienes este test:
def test_parse_config_returns_dict():
content = "key=value\nother=123"
result = parse_config(content)
assert result == {"key": "value", "other": "123"}
Escribe un prompt completo para Claude Code que incluya especificación, constraints e interfaz. Assume que parse_config vivirá en config/loader.py y será usada por main.py para cargar variables.
Ver solución
Implementa parse_config(content: str) -> dict[str, str] en config/loader.py.
Test a satisfacer (tests/test_config.py):
def test_parse_config_returns_dict():
content = "key=value\nother=123"
result = parse_config(content)
assert result == {"key": "value", "other": "123"}
Contexto: main.py hace:
config = parse_config(Path("config.env").read_text())
db_url = config["database_url"]
Constraints:
- Solo stdlib
- Líneas vacías deben ignorarse
- Líneas sin "=" deben ignorarse o lanzar ValueError (elige uno y documéntalo)
- Keys sin valor (línea "key=") deben mapear a ""
Ejercicio 3: Aplicar el framework de decisión (Medio)
Claude implementa slugify. El test test_slugify_removes_accents falla:
def test_slugify_removes_accents():
assert slugify("café") == "cafe"
Claude retorna slugify("café") == "caf" (trunca en el acento). ¿Es problema de test o de implementación? ¿Qué acción tomarías?
Ver solución
Diagnóstico: El test es correcto. Queremos que los acentos se normalicen (é → e), no que se eliminen truncando.
Problema: Implementación. Claude probablemente filtró caracteres no-ASCII de forma naive en vez de usar unicodedata.normalize.
Acción: Dar más contexto: "Usa unicodedata para normalizar acentos (NFC/NFD) antes de slugificar. 'café' debe resultar en 'cafe'."
O dejarlo iterar con el error: "El test espera que 'café' se convierta en 'cafe', pero tu implementación retorna 'caf'. Corrige para normalizar caracteres acentuados."
Ejercicio 4: Detectar over-engineering (Medio)
Claude Code genera esto para un test que solo pide "retornar el primer elemento de una lista":
class ListUtils:
@staticmethod
def first(lst: list) -> any:
if not isinstance(lst, list):
raise TypeError("Expected list")
if len(lst) == 0:
raise ValueError("List cannot be empty")
return lst[0]
El test era: assert first([1,2,3]) == 1. ¿Qué constraint agregarías al siguiente prompt para evitar este over-engineering?
Ver solución
Constraints útiles:
- "Implementación mínima. Una función, no una clase. Solo el código necesario para pasar los tests."
- "No agregar validaciones que los tests no pidan. Si el test no valida listas vacías, no agregues ese check todavía."
- "Una función de 2-3 líneas máximo para este caso."
El principio: los tests definen qué validar. Si no hay test para "lista vacía", no agregar ese manejo hasta que escribas el test.
Ejercicio 5: Evitar hardcoding con parametrize (Difícil)
Tienes un test que Claude "pasa" con hardcoding:
def test_calculate_discount():
assert calculate_discount(100, 10) == 90
Claude implementa: def calculate_discount(price, pct): return 90. Reescribe el test usando @pytest.mark.parametrize para que un hardcode obvio falle.
Ver solución
import pytest
@pytest.mark.parametrize("price, pct, expected", [
(100, 10, 90),
(200, 25, 150),
(50, 50, 25),
(100, 0, 100),
(100, 100, 0),
])
def test_calculate_discount(price, pct, expected):
assert calculate_discount(price, pct) == expected
Con múltiples casos, return 90 falla en el segundo y tercer caso. Parametrize obliga a implementar la lógica real.
Ejercicio 6: Flujo iterativo completo (Difícil)
Simula 3 ciclos TDD para una función validate_email(email: str) -> bool:
- Ciclo 1: test que pasa (email válido retorna True)
- Ciclo 2: test que falla inicialmente (email inválido retorna False) — Claude itera
- Ciclo 3: test que falla (email con espacios retorna False) — Claude itera
Escribe los tres tests, el prompt del ciclo 1, y los prompts de iteración para ciclos 2 y 3.
Ver solución
Tests:
def test_valid_email_returns_true():
assert validate_email("user@example.com") is True
def test_invalid_email_returns_false():
assert validate_email("notanemail") is False
def test_email_with_spaces_returns_false():
assert validate_email("user @example.com") is False
Prompt ciclo 1:
Implementa validate_email(email: str) -> bool en utils/validation.py.
Debe pasar: assert validate_email("user@example.com") is True
Solo stdlib. Implementación mínima.
Prompt iteración ciclo 2: (asumiendo que Claude retornaba True para todo)
El test test_invalid_email_returns_false falla. validate_email("notanemail")
debe retornar False porque no tiene @. Corrige la implementación.
Prompt iteración ciclo 3:
El test test_email_with_spaces_returns_false falla. Un email con espacios
("user @example.com") no es válido y debe retornar False. Ajusta la validación.
Resumen
- ✅ El prompt de implementación debe incluir: tests, constraints e interfaces.
- ✅ Patrón: "Implementa [X] para que pase estos tests: [tests]. Constraints: [lista]."
- ✅ Evaluar no solo si pasan tests, sino si el código es correcto, no hace trampa y no sobre-ingenieriza.
- ✅ Framework de decisión: si el test es correcto → problema de implementación (contexto, enfoque o iteración); si el test no es correcto → problema de spec (clarificar, simplificar o agregar constraint).
- ✅ Dar contexto efectivo: test + código existente + constraints explícitos.
- ✅ Loop: test → prompt → falla? → evaluar → re-prompt o ajustar test → repetir hasta verde.
- ✅ Heurísticas: mismo error dos veces → cambiar prompt; código malo → más tests; over-engineering → constraint de minimalidad; hardcoding → parametrize.
- ✅ El ciclo de implementación con Claude Code se vuelve fluido con práctica: escribir specs claras, dar contexto rico y saber cuándo intervenir son las tres piernas del skill.
Próxima cápsula: Refactoring con tests como safety net — mejorar el código sin romper funcionalidad.
Recursos Adicionales
- Anthropic: Claude Code Best Practices - Cómo trabajar efectivamente con Claude Code
- Tweag: Spec-Driven Development with LLMs - Spec-first y desarrollo con LLMs
- Martin Fowler: Test-Driven Development - Fundamentos del ciclo red-green-refactor
- pytest: Parametrize - Tests parametrizados para evitar hardcoding
- Kent Beck: TDD by Example - El libro fundacional de TDD
- Google: Testing Blog - Test-Driven Development - Artículos sobre TDD y prácticas de testing
Módulo 4, Cápsula 04 — Testing with Claude Code Guide
El ciclo verde: prompts efectivos, evaluación crítica y juicio para intervenir