Módulo 4: TDD Workflow Completo
Writing Failing Tests First — El Arte del Test Rojo
Writing Failing Tests First — El Arte del Test Rojo
Descripción de la cápsula
En la cápsula anterior entendiste el ciclo red-green-refactor adaptado para AI: Claude Code implementa, tú defines qué debe pasar con tests. Pero hay un detalle crítico que separa TDD real de TDD superficial: el test debe fallar primero.
Un test que pasa inmediatamente no prueba nada. Un test que falla (rojo) y luego pasa (verde) después de la implementación es la prueba de que el test estaba probando lo correcto y de que el código lo soluciona. Esta cápsula te enseña el arte de escribir tests rojos efectivos: tests que fallen por la razón correcta, que tengan el tamaño adecuado, y que le den a Claude Code información clara para implementar.
Al final, sabrás escribir tests que documentan exactamente lo que quieres, en ciclos incrementales que no abruman a Claude Code ni a ti mismo.
Por Qué el Test DEBE Fallar Primero
Un test que pasa inmediatamente no prueba nada
Imagina que escribes este test:
# test_calculator.py
from calculator import add
def test_add_numbers():
assert add(2, 3) == 5
Luego pides a Claude Code: "Implementa la función add para que pase este test." Claude genera:
# calculator.py
def add(a: float, b: float) -> float:
return 5 # Hardcoded!
Ejecutas pytest → pasa. ¿Confías en el código? No. Porque nunca viste el test fallar. No sabes si el test está verificando la suma real o si está verificando que retorna 5.
La regla de oro: si un test pasa sin que hayas implementado nada (o con una implementación trivial), ese test es inútil.
El test que falla PROBÓ que está probando lo correcto
El flujo correcto:
1. Escribes test_add_numbers que espera add(2, 3) == 5
2. add no existe → ImportError o NameError
3. Claude Code implementa add(a, b): return a + b
4. pytest → pasa
Ahora SÍ confías: el test falló porque la función no existía o no hacía lo correcto. La implementación lo arregló. La transición rojo → verde es la prueba de progreso.
El test que falla DEFINE qué significa éxito
En TDD, el test no verifica código existente — define el contrato. El test dice: "cuando la función reciba X, debe retornar Y." Hasta que el test pase, la feature no existe. El test rojo es la especificación viva.
Red → Green es la prueba de progreso
Cada transición rojo → verde es un incremento verificable. Si escribes 5 tests y todos pasan al primer prompt a Claude Code, no tienes evidencia de que esos tests hacen algo útil. Si cada test falla primero y luego pasa tras la implementación, tienes 5 evidencias de que el código hace lo que los tests dicen.
Anatomía de un Buen Test que Falla
Imports la función que no existe aún (ImportError → ¡bueno!)
Un test que importa algo inexistente falla inmediatamente. Eso es exactamente lo que quieres.
# test_inventory.py
import pytest
from inventory import add_item, remove_item, get_stock # Inventory no existe aún
def test_add_item_increases_stock():
add_item("widget", 10)
assert get_stock("widget") == 10
Al ejecutar pytest, obtienes:
ImportError: cannot import name 'add_item' from 'inventory'
Ese fallo es positivo: confirma que el test está intentando usar la API que esperas. Claude Code leerá el test y creará el módulo inventory con add_item, remove_item, get_stock.
Assertión clara sobre el comportamiento esperado
# ✅ BIEN: expectativa clara
def test_register_with_valid_email_creates_user():
user = register_user(email="alice@example.com", password="secret123")
assert user.email == "alice@example.com"
assert user.id is not None
# ❌ MAL: assert vago
def test_register_works():
user = register_user("alice@example.com", "secret")
assert user # ¿Qué exactamente?
La assertión debe ser específica: valor exacto, tipo, excepción con mensaje. Claude Code lee el test para saber qué implementar; una assertión vaga le da lugar a interpretación errónea.
Alcance pequeño — UN comportamiento por test
Un test que valida muchas cosas es difícil de interpretar cuando falla. ¿Qué parte falló?
# ❌ MAL: múltiples comportamientos
def test_authentication_system():
user = register("alice@example.com", "pass")
assert user is not None
token = login("alice@example.com", "pass")
assert token is not None
decoded = decode_token(token)
assert decoded["user_id"] == user.id
# Si falla, ¿registro? ¿login? ¿decode?
# ✅ BIEN: un comportamiento
def test_register_with_valid_email_creates_user():
user = register("alice@example.com", "pass")
assert user.email == "alice@example.com"
def test_login_with_valid_credentials_returns_token():
register("alice@example.com", "pass")
token = login("alice@example.com", "pass")
assert token is not None
assert isinstance(token, str)
Nombre descriptivo que documenta la intención
El nombre del test es documentación. Al leer test_register_with_valid_email_creates_user sabes exactamente qué se prueba. Al leer test_register_works no.
Regla: el nombre debe permitir deducir el escenario y el resultado esperado sin leer el cuerpo.
Tests Demasiado Grandes vs Demasiado Pequeños
DEMASIADO GRANDE: test_authentication_system_works
# ❌ Prueba todo de una vez
def test_authentication_system_works():
# Registro
user = register("a@b.com", "pass")
# Login
token = login("a@b.com", "pass")
# Validación
decoded = decode(token)
assert decoded["user_id"] == user.id
# Refresh
new_token = refresh(token)
assert new_token != token
# Logout
logout(new_token)
assert validate(new_token) is False
Si falla, no sabes dónde. Si Claude Code implementa mal una parte, el feedback es confuso. Además, es imposible ciclar: no puedes "implementar solo registro" porque el test exige todo.
DEMASIADO PEQUEÑO: test_returns_true
# ❌ Sin significado
def test_returns_true():
assert is_valid("x") == True
El test "prueba" algo, pero no documenta qué comportamiento se espera. ¿Validación de qué? ¿Bajo qué reglas?
JUSTO: test_register_with_valid_email_creates_user
# ✅ Un escenario, un resultado, nombre descriptivo
def test_register_with_valid_email_creates_user():
user = register_user(email="alice@example.com", password="secret123")
assert user.email == "alice@example.com"
assert user.id is not None
Regla práctica: cada test debe fallar por exactamente UNA razón. Si un test puede fallar porque falta A, porque falta B, o porque falta C, divídelo en tres tests.
Checklist rápida antes de pedir implementación a Claude Code
Antes de dar un test rojo a Claude Code, verifica:
- ✅ El test falla actualmente (ImportError o AssertionError)
- ✅ El nombre del test describe el escenario y el resultado esperado
- ✅ La assertión es específica (valor exacto, tipo, o excepción con mensaje)
- ✅ El test tiene un solo propósito
- ✅ Los imports apuntan al módulo que Claude Code debe crear o modificar
Escritura Incremental de Tests para una Feature
Ejemplo: Construyendo un verificador de fortaleza de contraseña
Supongamos que quieres check_strength(password: str) -> str que retorna "weak", "medium" o "strong".
Ciclo 1: Longitud básica
Escribes el primer test:
# test_password_strength.py
import pytest
from password_strength import check_strength
def test_password_too_short_returns_weak():
assert check_strength("abc") == "weak"
Ejecutas pytest → falla (ImportError o AssertionError). Le pides a Claude Code que implemente. Claude genera:
# password_strength.py
def check_strength(password: str) -> str:
if len(password) < 8:
return "weak"
return "medium"
pytest pasa. Ciclo 1 completo.
Ciclo 2: Mayúsculas y minúsculas
Añades otro test:
def test_password_mixed_case_returns_medium():
assert check_strength("AbcDef12") == "medium"
Ejecutas → puede pasar ya (8 chars). Pero quieres diferenciar "medium" de "strong". Ajustas la especificación: "strong" requiere mayúsculas, minúsculas, números Y caracteres especiales.
def test_password_mixed_case_and_numbers_returns_medium():
assert check_strength("AbcDef12") == "medium"
La implementación actual ya retorna "medium" para eso. Añades:
def test_password_only_lowercase_long_returns_weak():
assert check_strength("abcdefgh") == "weak"
Ahora falla. Claude Code actualiza:
def check_strength(password: str) -> str:
if len(password) < 8:
return "weak"
has_upper = any(c.isupper() for c in password)
has_lower = any(c.islower() for c in password)
has_digit = any(c.isdigit() for c in password)
if has_upper and has_lower and has_digit:
return "strong"
return "medium"
Espera: el test pedía "abcdefgh" → weak. La implementación podría dar "medium" (8 chars, lowercase). Necesitas refinar: "weak" = sin mayúsculas O sin números. La lógica de "medium" vs "strong" se define con tests. Cada test añade un requisito nuevo.
Ciclo 3: Caracteres especiales
def test_password_with_special_chars_returns_strong():
assert check_strength("AbcDef12!@") == "strong"
Fallará hasta que la implementación exija caracteres especiales para "strong". Claude Code ajusta la lógica.
Resumen del enfoque incremental
- ✅ Cada test añade un requisito nuevo
- ✅ La implementación de Claude Code crece incrementalmente
- ✅ Cada ciclo es corto: un test → implementar → validar
- ✅ Si escribes todos los tests de golpe, pierdes el beneficio del feedback inmediato
Tabla de progresión del ejemplo
| Ciclo | Test nuevo | Implementación resultante |
|---|---|---|
| 1 | test_password_too_short_returns_weak | len < 8 → "weak" |
| 2 | test_password_only_lowercase_long_returns_weak | Validación de mayúsculas/números |
| 3 | test_password_with_special_chars_returns_strong | Criterio completo para "strong" |
Cada fila representa un ciclo red-green independiente. Nunca avanzas al siguiente test hasta que el anterior pase.
Qué Hace que un Test que Falla sea ÚTIL para Claude Code
Firma de función clara (qué función, qué argumentos)
# ✅ Claude Code sabe: función add_item, args (name, quantity)
def test_add_item_increases_stock():
add_item("widget", 10)
assert get_stock("widget") == 10
Si el test usa una API inventada o inconsistente, Claude Code puede implementar algo que no encaja con el resto del sistema.
Salida esperada clara (valor exacto o excepción)
# ✅ Valor exacto
assert check_strength("abc") == "weak"
# ✅ Excepción con tipo y mensaje
with pytest.raises(InsufficientStockError) as exc_info:
remove_item("widget", 100)
assert "insufficient stock" in str(exc_info.value).lower()
Contexto mediante el nombre
El nombre test_register_with_valid_email_creates_user le dice a Claude Code: se prueba el registro, con email válido, y el resultado debe ser un usuario creado.
Edge cases como tests separados
No mezcles muchos casos en un solo test. Cada edge case = un test.
def test_remove_item_from_empty_inventory_raises():
with pytest.raises(InsufficientStockError):
remove_item("widget", 1)
def test_remove_item_more_than_stock_raises():
add_item("widget", 5)
with pytest.raises(InsufficientStockError):
remove_item("widget", 10)
def test_remove_item_exact_stock_empties_item():
add_item("widget", 5)
remove_item("widget", 5)
assert get_stock("widget") == 0
La Trampa de "Demasiado de Una Vez"
Escribir 20 tests antes de implementar nada
Si escribes todos los tests de una feature compleja antes de pedir implementación, Claude Code recibe un bloque grande con posibles contradicciones o prioridades poco claras. ¿Qué implementar primero? ¿Qué es más importante?
❌ Flujo problemático:
1. Escribes 20 tests para auth (register, login, tokens, refresh, logout, edge cases)
2. "Claude, implementa todo para que pasen"
3. Claude genera 200 líneas
4. 12 tests pasan, 8 fallan
5. Los 8 fallos tienen causas distintas → Claude se confunde al intentar arreglar todo
Mejor: 2-3 tests → implementar → 2-3 más → implementar
✅ Flujo efectivo:
1. Escribes: test_register_creates_user, test_register_duplicate_email_fails
2. "Claude, implementa registro para que pasen estos tests"
3. Claude implementa → 2/2 pasan
4. Escribes: test_login_returns_token, test_login_wrong_password_fails
5. "Claude, implementa login para que pasen estos tests"
6. Claude implementa → 4/4 pasan
7. Repites para tokens, refresh, etc.
El sweet spot: suficientes tests para definir el comportamiento, no tantos que Claude Code tenga restricciones contradictorias.
Heurística práctica
- Si tienes 1-3 tests → suele estar bien para un ciclo
- Si tienes 4-6 tests → aún manejable si son coherentes
- Si tienes 7+ tests para una sola feature en el primer ciclo → considera dividir en sub-features o ciclos separados
Práctica: Construir una Feature Test-First
Caso: Tracker de inventario
Vamos a construir un módulo inventory con las operaciones:
add_item(name, quantity)— añade stockremove_item(name, quantity)— quita stock, lanza si no hay suficienteget_stock(name)— retorna cantidad en stock
Paso 1: Test para add_item y get_stock
Crea test_inventory.py:
# test_inventory.py
import pytest
from inventory import add_item, remove_item, get_stock
def test_add_item_increases_stock():
add_item("widget", 10)
assert get_stock("widget") == 10
Ejecuta pytest test_inventory.py -v. Debe fallar (ImportError).
Prompt a Claude Code:
Tengo este test que falla. Implementa el módulo
inventorycon las funcionesadd_item,remove_item, yget_stockpara que pase. Usa un diccionario en memoria para almacenar el stock.
Claude genera:
# inventory.py
_stock: dict[str, int] = {}
def add_item(name: str, quantity: int) -> None:
_stock[name] = _stock.get(name, 0) + quantity
def remove_item(name: str, quantity: int) -> None:
current = _stock.get(name, 0)
if current < quantity:
raise ValueError("Insufficient stock")
_stock[name] -= quantity
def get_stock(name: str) -> int:
return _stock.get(name, 0)
pytest → pasa. Ciclo 1 completo.
Nota sobre aislamiento: En un proyecto real, usarías @pytest.fixture(autouse=True) o un setUp/tearDown para limpiar _stock entre tests y evitar que un test afecte a otro. Para este ejemplo didáctico, el orden de los tests y los datos elegidos evitan colisiones.
Paso 2: Test para remove_item
Añade:
def test_remove_item_decreases_stock():
add_item("widget", 10)
remove_item("widget", 3)
assert get_stock("widget") == 7
pytest → pasa (la implementación ya lo soporta).
Paso 3: Test para stock insuficiente
class InsufficientStockError(Exception):
pass
Necesitas definir la excepción. Actualiza el test:
def test_remove_item_more_than_stock_raises():
add_item("widget", 5)
with pytest.raises(ValueError): # O InsufficientStockError si la defines
remove_item("widget", 10)
Si la implementación usa ValueError, el test pasa. Si quieres una excepción personalizada, añade InsufficientStockError en inventory y actualiza el test para usar pytest.raises(InsufficientStockError).
Paso 4: Test para item inexistente
def test_get_stock_nonexistent_item_returns_zero():
assert get_stock("nonexistent") == 0
pytest → pasa (.get(name, 0) ya retorna 0).
Paso 5: Ejecutar el flujo completo
Estructura final del proyecto:
inventory-tracker/
├── inventory.py # Módulo implementado por Claude Code
├── test_inventory.py # Tests escritos por ti (test-first)
└── venv/
Comando para correr los tests tras cada cambio:
pytest test_inventory.py -v
Output esperado cuando todos pasan:
test_inventory.py::test_add_item_increases_stock PASSED
test_inventory.py::test_remove_item_decreases_stock PASSED
test_inventory.py::test_remove_item_more_than_stock_raises PASSED
test_inventory.py::test_get_stock_nonexistent_item_returns_zero PASSED
Resumen del flujo práctico
- Escribe un test que falla
- Ejecuta pytest (rojo)
- Da el test + contexto a Claude Code
- Claude implementa
- Ejecuta pytest (verde)
- Añade el siguiente test y repite
Prompt sugerido para Claude Code
Cuando pidas la implementación, incluye:
- El archivo de test completo (o los tests relevantes)
- La instrucción: "Implementa el módulo X para que estos tests pasen"
- Restricciones: "usa memoria en lugar de base de datos", "la API debe ser exactamente la que usan los tests"
Ejemplo completo:
Tengo
test_inventory.pycon tests que fallan por ImportError. Implementainventory.pycon las funcionesadd_item(name, quantity),remove_item(name, quantity)yget_stock(name). Usa un diccionario en memoria.remove_itemdebe lanzarValueErrorcuando no haya stock suficiente. El módulo no debe tener estado global persistente entre tests (cada test puede empezar con inventario vacío, o usa un patrón que permita reset).
Ejercicios
Ejercicio 1: Identificar tests demasiado grandes (Fácil)
Revisa este test y divídelo en tests más pequeños. Escribe los nombres de los nuevos tests.
def test_user_profile_system():
user = create_user("alice", "alice@example.com")
user.update_profile(bio="Hello")
user.add_avatar("avatar.png")
profile = get_profile(user.id)
assert profile["bio"] == "Hello"
assert profile["avatar"] == "avatar.png"
user.delete_avatar()
assert get_profile(user.id)["avatar"] is None
Ver solución
Posible división:
- ✅
test_create_user_stores_name_and_email - ✅
test_update_profile_stores_bio - ✅
test_add_avatar_stores_avatar_url - ✅
test_get_profile_returns_bio_and_avatar - ✅
test_delete_avatar_removes_avatar_from_profile
Cada test debe tener un solo propósito. Si test_user_profile_system falla, no sabes si el problema está en create, update, add_avatar, get_profile o delete_avatar.
Ejercicio 2: Escribir el primer test rojo (Fácil)
Quieres una función validate_email(email: str) -> bool que retorne True para emails válidos y False para inválidos. Escribe UN test que deba fallar primero (la función no existe aún) y que defina claramente un caso válido.
Ver solución
# test_email_validation.py
import pytest
from email_validator import validate_email
def test_valid_email_with_at_and_domain_returns_true():
assert validate_email("user@example.com") is True
Al ejecutar pytest obtendrás ImportError. Ese fallo es correcto: confirma que el test está probando la API que quieres.
Ejercicio 3: Ciclo incremental para un parser (Medio)
Quieres parse_duration(s: str) -> int que convierte strings como "5m", "1h", "30s" a segundos. Diseña 3 tests en orden incremental (cada uno añade un requisito). Escribe los tests y describe brevemente la implementación después de cada ciclo.
Ver solución
Ciclo 1: Solo segundos
def test_parse_duration_seconds():
assert parse_duration("30s") == 30
Implementación: int(s.replace("s", "")) o similar.
Ciclo 2: Minutos
def test_parse_duration_minutes():
assert parse_duration("5m") == 300
Implementación: detectar "m" y multiplicar por 60.
Ciclo 3: Horas
def test_parse_duration_hours():
assert parse_duration("1h") == 3600
Implementación: detectar "h" y multiplicar por 3600.
Orden recomendado: empezar por la unidad más simple (segundos) e ir añadiendo complejidad.
Ejercicio 4: Sweet spot de cantidad de tests (Medio)
Estás construyendo un endpoint POST /tasks que acepta {"title": str, "priority": int}. ¿Cuántos tests escribirías en el primer ciclo antes de pedir implementación a Claude Code? Justifica.
Ver solución
Recomendación: 2-3 tests en el primer ciclo
Ejemplos:
test_create_task_with_valid_data_returns_201_and_tasktest_create_task_missing_title_returns_400test_create_task_invalid_priority_returns_400
Con 2-3 tests defines: happy path + 1-2 validaciones críticas. No necesitas todos los edge cases (priority negativo, title vacío, etc.) en el primer ciclo.
Evita: 10+ tests en el primer ciclo. Si hay demasiados, Claude Code puede perderse o priorizar mal.
Ejercicio 5: Diagnosticar por qué un test no falla (Medio)
Escribiste este test y, para tu sorpresa, pasó antes de implementar nada:
def test_calculate_total_includes_tax():
assert calculate_total(100, 0.1) == 110
Investigas y descubres que existe una función calculate_total en otro módulo que se importa automáticamente. ¿Qué hacer?
Ver solución
Opciones:
-
Importar explícitamente desde tu módulo (si estás construyendo uno nuevo):
from my_module.calculator import calculate_totalSi
my_module.calculatorno existe o no tiene la función, el test fallará. -
Renombrar la función en tu spec para evitar el conflicto (ej.
calculate_total_with_tax). -
Usar un namespace de test: Crear un módulo vacío o stub que no tenga la función, y asegurarte de que el test importe de ahí.
El objetivo: el test debe fallar hasta que TU implementación (o la de Claude Code para tu módulo) exista y sea correcta.
Ejercicio 6: Redactar un prompt efectivo para Claude Code (Medio)
Tienes este test que falla:
def test_search_products_filters_by_category():
add_product("Widget A", category="electronics")
add_product("Widget B", category="clothing")
results = search_products(category="electronics")
assert len(results) == 1
assert results[0]["name"] == "Widget A"
Escribe un prompt corto (2-4 líneas) que le darías a Claude Code para implementar la funcionalidad.
Ver solución
Ejemplo de prompt efectivo:
Tengo este test que falla. Implementa las funciones
add_product,search_productsy cualquier estructura de datos necesaria para que pase. Usa almacenamiento en memoria (lista o dict).add_productrecibe nombre y categoría.search_productsfiltra por categoría y retorna lista de productos con al menos el campo "name".
Incluye:
- Que el test falla (contexto)
- Qué funciones implementar
- Restricciones (memoria, estructura de datos)
- Formato esperado del resultado
Anti-patrones a evitar
Al escribir tests rojos para TDD con Claude Code, evita:
- ❌ Test que nunca falló: Si no viste el test en rojo, no sabes si está probando algo real
- ❌ Test que verifica implementación: Ej. assert que una función interna fue llamada N veces
- ❌ Test monolítico: Un test que valida registro + login + tokens en una sola función
- ❌ Demasiados tests en un ciclo: Más de 5-6 tests nuevos antes de implementar
- ❌ Assertiones vagas:
assert resultoassert usersin especificar qué propiedad
Troubleshooting
Problema 1: "Mi test pasa pero no implementé nada"
Causa: El test importa una función que ya existe en otro lugar, o el assert es trivial (assert True).
Solución: Verifica que el test importe desde el módulo que vas a crear o modificar. Usa rutas de import explícitas. Si el test pasa sin implementación, el test no está probando lo que crees.
Problema 2: Claude Code implementa algo que hace pasar el test de forma incorrecta
Ejemplo: El test espera add(2, 3) == 5 y Claude genera def add(a, b): return 5.
Solución: Añade un segundo test que fuerce el comportamiento real: assert add(1, 4) == 5. Un solo test puede ser "adivinado" con hardcoding. Dos tests con inputs distintos hacen evidente que la lógica debe ser correcta.
Problema 3: "Escribí 15 tests y Claude Code no sabe por dónde empezar"
Causa: Demasiados tests a la vez generan contradicciones o prioridades poco claras.
Solución: Oculta o comenta tests. Deja solo 2-3 activos, pide implementación, y cuando pasen, activa los siguientes. Ciclos pequeños funcionan mejor.
Problema 4: El test falla por ImportError pero no sé si es el error "correcto"
Aclaración: ImportError es un fallo válido. Significa que el test está intentando usar código que no existe. Lo importante es que, una vez Claude Code implemente el módulo, el fallo cambie: de ImportError a (idealmente) nada, o a AssertionError si la lógica es incorrecta. Si después de implementar sigues con ImportError, el problema está en rutas de import o nombres de archivos.
Problema 5: "Mi test es muy específico y ahora el código está acoplado a detalles de implementación"
Ejemplo: El test verifica que se llama a una función interna en un orden concreto, en vez de verificar el resultado.
Solución: Enfoca el test en el comportamiento observable (output, efectos secundarios observables), no en cómo se implementa. Si el test requiere que exista _internal_helper(), estás probando implementación, no contrato. Reformula el test para verificar el resultado final.
Problema 6: Tests pasan por separado pero fallan juntos
Causa: Estado compartido entre tests (variables globales, archivos, base de datos) que no se limpia entre ejecuciones.
Solución: Usa fixtures con autouse=True para resetear el estado, o ejecuta cada test en un proceso aislado con pytest -x para debugear el primero que falle. En el inventario, una función reset_inventory() llamada en un fixture soluciona el problema.
Conexión con Proyecto
En el proyecto de este módulo (Feature completa con TDD), construirás un sistema de autenticación (registro, login, tokens) usando TDD con Claude Code:
- ✅ Cada aspecto de auth empezará con tests que fallan:
test_register_creates_user,test_login_returns_token,test_expired_token_raises_error, etc. - ✅ Escribirás 2-3 tests por ciclo, pedirás implementación a Claude Code, y validarás antes de añadir más.
- ✅ Los tests rojos que escribas aquí siguen las mismas reglas: un propósito por test, nombre descriptivo, expectativas claras.
La cápsula 04 cubre el ciclo de implementación con Claude Code: cómo dar contexto, cuándo iterar y cuándo intervenir.
Resumen
En esta cápsula aprendiste:
- ✅ Un test que pasa de inmediato no demuestra nada; el que falla primero y luego pasa prueba que el test y la implementación son correctos
- ✅ Un buen test rojo importa lo que no existe, tiene una assertión clara, un solo propósito y un nombre descriptivo
- ✅ Evita tests demasiado grandes (difíciles de depurar) y demasiado pequeños (sin significado)
- ✅ Escribe tests de forma incremental: 2-3 tests → implementar → 2-3 más → implementar
- ✅ Un test útil para Claude Code tiene firma clara, salida esperada explícita y contexto en el nombre
- ✅ El sweet spot: suficientes tests para definir el comportamiento, sin saturar a Claude Code con restricciones contradictorias
Próxima cápsula: Ciclo de implementación con Claude Code — dar contexto, evaluar resultados, iterar y decidir cuándo intervenir.
Checklist final
Antes de pasar a la siguiente cápsula, deberías poder:
- ✅ Explicar por qué un test que pasa inmediatamente no prueba nada
- ✅ Escribir un test que falle por la razón correcta (función inexistente o lógica incorrecta)
- ✅ Distinguir un test demasiado grande de uno demasiado pequeño de uno "justo"
- ✅ Diseñar 2-3 tests para el primer ciclo de una feature nueva
- ✅ Seguir el flujo: test rojo → Claude implementa → pytest verde → siguiente test
Recursos Adicionales
- Test Driven Development: By Example (Kent Beck) - Origen del TDD y la importancia del test rojo
- pytest: How to write and run tests - Documentación oficial de pytest para estructura de tests
- The Cycle of TDD (Red-Green-Refactor) - Robert C. Martin sobre el ciclo TDD
- Articulo: Why TDD with AI is different - Spec-driven development con LLMs (Tweag)
- Refactoring with Tests (Martin Fowler) - Uso de tests como base segura para refactoring
- Writing Good Tests (Microsoft DevBlog) - Principios para tests claros y mantenibles
Módulo 4, Cápsula 03 — Testing with Claude Code Guide