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

CicloTest nuevoImplementación resultante
1test_password_too_short_returns_weaklen < 8 → "weak"
2test_password_only_lowercase_long_returns_weakValidación de mayúsculas/números
3test_password_with_special_chars_returns_strongCriterio 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 stock
  • remove_item(name, quantity) — quita stock, lanza si no hay suficiente
  • get_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 inventory con las funciones add_item, remove_item, y get_stock para 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

  1. Escribe un test que falla
  2. Ejecuta pytest (rojo)
  3. Da el test + contexto a Claude Code
  4. Claude implementa
  5. Ejecuta pytest (verde)
  6. 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.py con tests que fallan por ImportError. Implementa inventory.py con las funciones add_item(name, quantity), remove_item(name, quantity) y get_stock(name). Usa un diccionario en memoria. remove_item debe lanzar ValueError cuando 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_task
  • test_create_task_missing_title_returns_400
  • test_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:

  1. Importar explícitamente desde tu módulo (si estás construyendo uno nuevo):

    from my_module.calculator import calculate_total

    Si my_module.calculator no existe o no tiene la función, el test fallará.

  2. Renombrar la función en tu spec para evitar el conflicto (ej. calculate_total_with_tax).

  3. 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_products y cualquier estructura de datos necesaria para que pase. Usa almacenamiento en memoria (lista o dict). add_product recibe nombre y categoría. search_products filtra 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 result o assert user sin 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

  1. Test Driven Development: By Example (Kent Beck) - Origen del TDD y la importancia del test rojo
  2. pytest: How to write and run tests - Documentación oficial de pytest para estructura de tests
  3. The Cycle of TDD (Red-Green-Refactor) - Robert C. Martin sobre el ciclo TDD
  4. Articulo: Why TDD with AI is different - Spec-driven development con LLMs (Tweag)
  5. Refactoring with Tests (Martin Fowler) - Uso de tests como base segura para refactoring
  6. Writing Good Tests (Microsoft DevBlog) - Principios para tests claros y mantenibles

Módulo 4, Cápsula 03 — Testing with Claude Code Guide