Módulo 1: Spec-First Methodology y TDD con AI
Anatomía de un Buen Test-Spec
Anatomía de un Buen Test-Spec
Descripción de la cápsula
Ya sabes POR QUÉ escribir tests primero (cápsula 02) y CÓMO funciona la metodología spec-first (cápsula 03). Pero no cualquier test funciona como buena especificación. Un test que es ambiguo, dependiente de otros tests, o no determinista produce una spec poco confiable — y Claude Code generará implementaciones igualmente poco confiables.
Esta cápsula te enseña las 5 características que hacen que un test sea una buena especificación: determinista, independiente, enfocado, con nombre descriptivo, y con un solo motivo de fallo. Son las reglas de diseño que separan un test útil de un test que ocupa espacio.
Al final, podrás evaluar cualquier test y determinar si funciona como spec-first — y sabrás corregir tests que no cumplen los criterios. Este es un skill que usarás en todos los módulos restantes de la guía.
Las 5 Características de un Buen Test-Spec
Característica 1: Determinista
Un test determinista produce el mismo resultado cada vez que se ejecuta — sin importar el orden, la hora, la máquina, o cuántas veces se corra.
# ✅ DETERMINISTA: Siempre produce el mismo resultado
def test_add_two_numbers():
assert add(2, 3) == 5
# ❌ NO DETERMINISTA: Depende de la hora
def test_greeting_message():
message = get_greeting()
assert message == "Buenos días" # Falla si son las 3pm
# ❌ NO DETERMINISTA: Depende de datos externos
def test_latest_user():
user = get_latest_user()
assert user.name == "John" # Falla si alguien creó otro user
# ❌ NO DETERMINISTA: Depende de random
def test_random_password():
password = generate_password()
assert len(password) == 12 # OK
assert password == "xK9!mP2@nQ4$" # Falla cada vez
¿Por qué importa para spec-first? Si el test no es determinista, Claude Code no puede saber si su implementación es correcta o si el test falló por factores externos. Una spec ambigua produce una implementación ambigua.
Cómo hacer tests deterministas:
# Problema: Depende de la hora
def test_greeting_depends_on_time():
message = get_greeting()
assert message == "Buenos días"
# Solución: Controla el input
def test_morning_greeting():
message = get_greeting(hour=9)
assert message == "Buenos días"
def test_afternoon_greeting():
message = get_greeting(hour=15)
assert message == "Buenas tardes"
# Problema: Depende de random
def test_random_password_bad():
password = generate_password()
assert password == "specific_password"
# Solución: Testea propiedades, no valores exactos
def test_password_length():
password = generate_password(length=12)
assert len(password) == 12
def test_password_has_uppercase():
password = generate_password(length=12, require_uppercase=True)
assert any(c.isupper() for c in password)
Característica 2: Independiente
Cada test debe poder ejecutarse solo, sin depender de otros tests. No debe asumir que otro test ya corrió, ni que ciertos datos existen en una base de datos.
# ❌ DEPENDIENTE: test_delete necesita que test_create haya corrido primero
class TestUserBad:
def test_create_user(self):
user = create_user("john@test.com")
assert user.id == 1
def test_delete_user(self):
delete_user(1) # Asume que user con id=1 existe
assert get_user(1) is None
# ✅ INDEPENDIENTE: Cada test crea su propio setup
class TestUserGood:
def test_create_user(self):
user = create_user("john@test.com")
assert user.email == "john@test.com"
def test_delete_user(self):
user = create_user("mary@test.com") # Crea su propio dato
delete_user(user.id)
assert get_user(user.id) is None
¿Por qué importa para spec-first? Si los tests son dependientes, Claude Code necesita entender el orden de ejecución — y pytest no garantiza orden. Una implementación que pasa tests en un orden puede fallar en otro.
Patrón: Arrange-Act-Assert (AAA)
El patrón AAA garantiza independencia al hacer que cada test tenga su propio setup:
def test_discount_calculation():
# ARRANGE: Preparar datos necesarios
price = 100.0
discount = 20
# ACT: Ejecutar la acción que testeas
result = calculate_discount(price, discount)
# ASSERT: Verificar el resultado
assert result == 80.0
Cada test tiene su propio Arrange — no depende del estado dejado por otro test.
Característica 3: Enfocado (una cosa por test)
Cada test debe verificar un solo comportamiento. Si el test verifica 5 cosas, cuando falla no sabes cuál de las 5 es el problema.
# ❌ DESENFOCADO: Testea todo en un test
def test_user_registration():
user = register("John", "john@test.com", "MyP@ss1!")
assert user.name == "John"
assert user.email == "john@test.com"
assert user.is_active == True
assert user.password != "MyP@ss1!" # Debe estar hasheado
assert len(user.password) == 60 # Largo de bcrypt hash
assert user.created_at is not None
# ✅ ENFOCADO: Un comportamiento por test
def test_register_stores_name():
user = register("John", "john@test.com", "MyP@ss1!")
assert user.name == "John"
def test_register_stores_email():
user = register("John", "john@test.com", "MyP@ss1!")
assert user.email == "john@test.com"
def test_register_activates_user():
user = register("John", "john@test.com", "MyP@ss1!")
assert user.is_active == True
def test_register_hashes_password():
user = register("John", "john@test.com", "MyP@ss1!")
assert user.password != "MyP@ss1!"
def test_register_sets_created_at():
user = register("John", "john@test.com", "MyP@ss1!")
assert user.created_at is not None
¿Por qué importa para spec-first? Cuando le das tests enfocados a Claude Code y uno falla, el error message dice exactamente qué comportamiento no funciona. Claude Code puede corregir el problema puntual en vez de revisar toda la implementación.
# Test desenfocado falla:
FAILED test_user_registration - AssertionError: assert 'MyP@ss1!' != 'MyP@ss1!'
→ Claude Code: "¿El problema es el hashing? ¿O algo más del test también falla?"
# Test enfocado falla:
FAILED test_register_hashes_password - AssertionError: assert 'MyP@ss1!' != 'MyP@ss1!'
→ Claude Code: "La password no se hashea. Necesito agregar hashing."
Característica 4: Nombre descriptivo
El nombre del test debe documentar el comportamiento que verifica. Sin leer el código del test, el nombre debe decirte qué pasa si el test falla.
# ❌ NOMBRES MALOS: No documentan comportamiento
def test_1():
...
def test_divide():
...
def test_edge_case():
...
def test_it_works():
...
# ✅ NOMBRES BUENOS: Documentan comportamiento
def test_divide_positive_numbers_returns_float():
...
def test_divide_by_zero_raises_value_error():
...
def test_divide_negative_by_positive_returns_negative():
...
def test_divide_with_float_inputs_maintains_precision():
...
Patrón de naming: test_[acción]_[contexto]_[resultado_esperado]
# Patrón: test_[qué hace]_[en qué condición]_[qué esperas]
def test_login_with_valid_credentials_returns_token():
...
def test_login_with_wrong_password_returns_401():
...
def test_login_with_nonexistent_email_returns_404():
...
def test_register_with_duplicate_email_raises_conflict():
...
¿Por qué importa para spec-first? Los nombres de tests son la documentación más confiable de tu sistema. Cuando alguien (o Claude Code) lee los nombres de tus tests, debe entender el comportamiento completo sin leer el código. Son tu tabla de contenidos del sistema.
pytest -v
test_auth.py::test_login_with_valid_credentials_returns_token PASSED
test_auth.py::test_login_with_wrong_password_returns_401 PASSED
test_auth.py::test_login_with_nonexistent_email_returns_404 PASSED
test_auth.py::test_register_with_duplicate_email_raises_conflict PASSED
test_auth.py::test_token_expires_after_one_hour PASSED
test_auth.py::test_expired_token_is_rejected_with_401 PASSED
Leyendo solo los nombres de los tests, entiendes completamente cómo funciona el sistema de autenticación.
Característica 5: Un solo motivo de fallo
Si un test puede fallar por múltiples razones diferentes, es difícil diagnosticar el problema. Cada test debe tener exactamente un motivo por el que puede fallar.
# ❌ MÚLTIPLES MOTIVOS DE FALLO:
def test_user_workflow():
# Puede fallar porque create_user falla
user = create_user("John", "john@test.com")
# Puede fallar porque update_user falla
updated = update_user(user.id, name="John Carter")
# Puede fallar porque la comparación falla
assert updated.name == "John Carter"
# ¿Cuál fue el problema si falla?
# ✅ UN SOLO MOTIVO:
def test_create_user_stores_name():
user = create_user("John", "john@test.com")
assert user.name == "John"
def test_update_user_changes_name():
user = create_user("John", "john@test.com") # Arrange
updated = update_user(user.id, name="John Carter") # Act
assert updated.name == "John Carter" # Assert
Nota: El segundo test sigue teniendo un create_user en el Arrange. Está bien — si create_user falla, el test falla con un error claro (create_user raised Exception), no con un assertion failure ambiguo. El motivo de fallo del assert es uno solo: update_user no cambió el nombre.
Anti-Patrones: Tests que NO Funcionan como Specs
Anti-patrón 1: Test que testea la implementación
# ❌ Testea CÓMO se implementa (acoplado a la implementación)
def test_sort_uses_quicksort():
import unittest.mock as mock
with mock.patch('sort_module.quicksort') as mock_qs:
sort_list([3, 1, 2])
mock_qs.assert_called_once()
# ✅ Testea QUÉ hace (desacoplado)
def test_sort_returns_ordered_list():
assert sort_list([3, 1, 2]) == [1, 2, 3]
def test_sort_empty_list():
assert sort_list([]) == []
def test_sort_already_sorted():
assert sort_list([1, 2, 3]) == [1, 2, 3]
¿Por qué es malo como spec? Si testeas que usa quicksort, Claude Code DEBE usar quicksort. Pero tal vez merge sort es mejor para este caso. La spec debe definir qué resultado produce, no cómo lo produce.
Anti-patrón 2: Test con lógica compleja
# ❌ El test tiene más lógica que el código que testea
def test_fibonacci():
expected = []
a, b = 0, 1
for _ in range(10):
expected.append(a)
a, b = b, a + b
assert fibonacci(10) == expected
# ✅ Valores explícitos
def test_fibonacci_first_10():
assert fibonacci(10) == [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
¿Por qué es malo como spec? Si el test calcula el resultado esperado con lógica, y esa lógica tiene un bug, el test pasa con una implementación incorrecta. Los valores esperados deben ser constantes conocidas, no calculados.
Anti-patrón 3: Test que ignora edge cases
# ❌ Solo happy path
def test_divide():
assert divide(10, 2) == 5
assert divide(6, 3) == 2
# ✅ Happy path + edge cases
def test_divide_basic():
assert divide(10, 2) == 5
def test_divide_by_zero():
with pytest.raises(ZeroDivisionError):
divide(10, 0)
def test_divide_zero_numerator():
assert divide(0, 5) == 0
def test_divide_negative():
assert divide(-10, 2) == -5
def test_divide_float_precision():
assert divide(1, 3) == pytest.approx(0.333333, rel=1e-4)
¿Por qué es malo como spec? Si la spec solo tiene happy path, Claude Code solo implementa happy path. Los edge cases que no especifiques quedan como comportamiento undefined — la AI decide qué hacer, y puede decidir mal.
Checklist: ¿Es mi test una buena spec?
Usa esta checklist antes de dar tus tests a Claude Code:
□ DETERMINISTA
¿Produce el mismo resultado cada vez que se ejecuta?
¿Depende de hora, fecha, datos externos, o random?
□ INDEPENDIENTE
¿Puede ejecutarse solo, sin que otros tests corran primero?
¿Crea su propio setup (Arrange)?
□ ENFOCADO
¿Verifica un solo comportamiento?
¿Si falla, sabes exactamente qué está mal?
□ NOMBRE DESCRIPTIVO
¿El nombre documenta el comportamiento?
¿Sin leer el código, entiendes qué verifica?
□ UN SOLO MOTIVO DE FALLO
¿Tiene exactamente una razón por la que puede fallar?
¿El assert verifica una sola cosa?
Si algún check falla, refactoriza el test antes de usarlo como spec.
Conexión con Proyecto
En el Spec-first mini-app (proyecto de este módulo):
- Cada test que escribas para la calculator pasará por esta checklist
- Verás que tests deterministas, independientes y enfocados producen implementaciones más precisas de Claude Code
- Practicarás naming que documenta comportamiento:
test_divide_by_zero_raises_value_erroren vez detest_error
Estos criterios de calidad se aplican en toda la guía — son la base de los unit tests (módulo 2), integration tests (módulo 3), y el proyecto final (módulo 8).
Troubleshooting
Problema 1: "Mis tests se sienten repetitivos"
Causa: Muchos tests que verifican lo mismo con variaciones mínimas.
Solución: Usa @pytest.mark.parametrize para agrupar variaciones (se cubre en detalle en módulo 2):
# En vez de 5 tests separados:
@pytest.mark.parametrize("input_val,expected", [
(0, 32),
(100, 212),
(37, 98.6),
(-40, -40),
])
def test_celsius_to_fahrenheit(input_val, expected):
assert celsius_to_fahrenheit(input_val) == pytest.approx(expected)
Problema 2: "No puedo hacer el test independiente sin duplicar mucho setup"
Causa: Setup complejo que cada test necesita.
Solución: Usa fixtures de pytest (se cubre en detalle en módulos 2 y 6):
import pytest
@pytest.fixture
def sample_user():
return create_user("test@test.com", "TestPass1!")
def test_update_name(sample_user):
updated = update_user(sample_user.id, name="New Name")
assert updated.name == "New Name"
def test_delete_user(sample_user):
delete_user(sample_user.id)
assert get_user(sample_user.id) is None
Problema 3: "No sé si mi test testea comportamiento o implementación"
Causa: Línea difusa entre ambos.
Solución: Pregúntate: "¿Si cambio la implementación interna pero el resultado es el mismo, mi test sigue pasando?" Si la respuesta es sí, testeas comportamiento. Si no, testeas implementación.
# Pregunta: Si cambio de quicksort a mergesort, ¿el test pasa?
def test_sort_uses_quicksort(): # NO → testea implementación ❌
def test_sort_returns_ordered(): # SÍ → testea comportamiento ✅
Ejercicios
Ejercicio 1: Evaluar tests (Fácil)
Para cada test, identifica qué característica de un buen test-spec NO cumple:
# Test A
def test_stuff():
result = process_data([1, 2, 3])
assert result is not None
# Test B
import random
def test_shuffle():
data = [1, 2, 3, 4, 5]
result = my_shuffle(data)
assert result == [3, 1, 5, 2, 4]
# Test C
user_id = None
def test_create():
global user_id
user = create_user("test@test.com")
user_id = user.id
assert user.id is not None
def test_get():
user = get_user(user_id)
assert user.email == "test@test.com"
Ver solución
Test A: Viola "nombre descriptivo" y "enfocado." El nombre test_stuff no documenta nada. El assert is not None es demasiado débil — casi cualquier implementación lo pasa.
Test B: Viola "determinista." my_shuffle produce un resultado diferente cada vez. El assert espera un orden específico que es imposible predecir.
Test C: Viola "independiente." test_get depende de que test_create haya corrido primero y guardado user_id en una variable global. Si pytest ejecuta test_get primero, falla.
Versiones corregidas:
# Test A corregido
def test_process_data_returns_sum():
assert process_data([1, 2, 3]) == 6
# Test B corregido
def test_shuffle_contains_all_elements():
result = my_shuffle([1, 2, 3, 4, 5])
assert sorted(result) == [1, 2, 3, 4, 5]
def test_shuffle_same_length():
result = my_shuffle([1, 2, 3, 4, 5])
assert len(result) == 5
# Test C corregido
def test_create_user():
user = create_user("test@test.com")
assert user.id is not None
def test_get_user_by_id():
user = create_user("test2@test.com")
retrieved = get_user(user.id)
assert retrieved.email == "test2@test.com"
Ejercicio 2: Mejorar nombres (Fácil)
Reescribe estos nombres de tests para que documenten el comportamiento:
def test_1():
assert validate_age(25) == True
def test_2():
assert validate_age(-1) == False
def test_3():
assert validate_age(0) == True
def test_error():
with pytest.raises(TypeError):
validate_age("twenty")
Ver solución
def test_validate_age_positive_number_returns_true():
assert validate_age(25) == True
def test_validate_age_negative_number_returns_false():
assert validate_age(-1) == False
def test_validate_age_zero_is_valid():
assert validate_age(0) == True
def test_validate_age_string_input_raises_type_error():
with pytest.raises(TypeError):
validate_age("twenty")
Explicación: Ahora al ejecutar pytest -v, la salida documenta completamente el comportamiento de validate_age:
test_validate_age_positive_number_returns_true PASSED
test_validate_age_negative_number_returns_false PASSED
test_validate_age_zero_is_valid PASSED
test_validate_age_string_input_raises_type_error PASSED
Sin leer una línea de código, sabes que validate_age acepta 0+, rechaza negativos, y lanza TypeError con strings.
Ejercicio 3: Hacer independientes (Medio)
Estos tests son dependientes. Refactorízalos para que sean independientes:
items = []
def test_add_item():
items.append({"name": "laptop", "price": 999})
assert len(items) == 1
def test_add_second_item():
items.append({"name": "mouse", "price": 29})
assert len(items) == 2
def test_total_price():
total = sum(item["price"] for item in items)
assert total == 1028
Ver solución
def test_add_item_to_empty_cart():
cart = []
cart.append({"name": "laptop", "price": 999})
assert len(cart) == 1
def test_add_two_items():
cart = []
cart.append({"name": "laptop", "price": 999})
cart.append({"name": "mouse", "price": 29})
assert len(cart) == 2
def test_total_price_of_two_items():
cart = [
{"name": "laptop", "price": 999},
{"name": "mouse", "price": 29},
]
total = sum(item["price"] for item in cart)
assert total == 1028
Explicación: Cada test crea su propio cart (Arrange). No hay estado compartido — cada test puede ejecutarse solo, en cualquier orden, y producir el mismo resultado.
Ejercicio 4: Escribir spec completa (Medio)
Necesitas una función slugify(text: str) -> str que convierta texto a formato URL slug. Ejemplo: "Hello World!" → "hello-world".
Escribe 8 tests que cumplan las 5 características de un buen test-spec.
Ver solución
import pytest
from text_utils import slugify
def test_slugify_simple_text():
assert slugify("Hello World") == "hello-world"
def test_slugify_converts_to_lowercase():
assert slugify("UPPERCASE") == "uppercase"
def test_slugify_replaces_spaces_with_hyphens():
assert slugify("hello world") == "hello-world"
def test_slugify_removes_special_characters():
assert slugify("Hello World!") == "hello-world"
def test_slugify_multiple_spaces_become_single_hyphen():
assert slugify("hello world") == "hello-world"
def test_slugify_strips_leading_trailing_spaces():
assert slugify(" hello world ") == "hello-world"
def test_slugify_empty_string():
assert slugify("") == ""
def test_slugify_accented_characters():
assert slugify("café résumé") == "cafe-resume"
Verificación contra checklist:
- ✅ Deterministas: mismo input → mismo output siempre
- ✅ Independientes: cada uno crea su propio input
- ✅ Enfocados: cada uno verifica un comportamiento específico
- ✅ Nombres descriptivos: documentan qué hace slugify
- ✅ Un solo motivo de fallo: cada assert verifica una cosa
Resumen
En esta cápsula aprendiste:
- ✅ Las 5 características de un buen test-spec: determinista, independiente, enfocado, nombre descriptivo, un solo motivo de fallo
- ✅ Patrón AAA (Arrange-Act-Assert) para garantizar independencia
- ✅ Naming convention
test_[acción]_[contexto]_[resultado]para documentar comportamiento - ✅ 3 anti-patrones: testear implementación, lógica compleja en tests, ignorar edge cases
- ✅ Checklist de validación para evaluar tus tests antes de usarlos como spec
- ✅ Tests enfocados producen feedback más preciso para Claude Code cuando algo falla
Próxima cápsula: Tu primer ciclo spec→implement con Claude Code — hands-on, de principio a fin.
Recursos Adicionales
- pytest: Good Practices - Mejores prácticas oficiales de pytest
- Arrange-Act-Assert Pattern - Explicación detallada del patrón AAA
- Test Desiderata (Kent Beck) - Las 12 propiedades de buenos tests, por el creador de TDD
- Writing Clean Tests - Uncle Bob sobre definiciones y prácticas de testing
- pytest Naming Conventions - Convenciones de naming para que pytest descubra tus tests
Módulo 1, Cápsula 04 — Testing with Claude Code Guide Buenos tests = buenas specs = buenas implementaciones