Módulo 1: Spec-First Methodology y TDD con AI
Proyecto del Módulo: Spec-First Mini-App (Calculator)
Proyecto del Módulo: Spec-First Mini-App (Calculator)
Descripción del proyecto
Has aprendido la filosofía spec-first, entendido por qué TDD importa más con AI, dominado la anatomía de un buen test-spec, y ejecutado tu primer ciclo spec→implement. Ahora es momento de integrar todo en un proyecto completo.
Vas a construir una calculator app usando spec-first methodology con Claude Code. No es una calculator trivial — incluye operaciones básicas, manejo de errores, edge cases como división por cero, y una función de historial de operaciones. El foco no es la complejidad de la calculator — es experimentar el workflow spec-first de principio a fin con un dominio familiar.
El proyecto sigue un flujo claro: defines tests para cada funcionalidad → das los tests a Claude Code → Claude Code implementa → pytest valida → iteras si es necesario → pasas a la siguiente funcionalidad. Al final, tendrás una calculator completa con 25+ tests como red de seguridad.
Este proyecto demuestra el patrón fundamental que usarás en toda la guía: tests como especificación, Claude Code como implementador, pytest como validador.
Objetivo del Proyecto
Construir una calculator con spec-first methodology donde TÚ defines el comportamiento con tests y Claude Code implementa la lógica.
Al completar este proyecto:
- ✅ Habrás ejecutado múltiples ciclos spec→implement→validate con Claude Code
- ✅ Tendrás una calculator con 25+ tests como documentación ejecutable del comportamiento
- ✅ Habrás experimentado iteration loops cuando tests fallan
- ✅ Tendrás un proyecto de referencia para el patrón spec-first
Especificaciones Técnicas
Stack Tecnológico
- Lenguaje: Python 3.10+
- Testing: pytest
- AI: Claude Code como implementador
- Dependencias: Solo pytest (sin dependencias adicionales)
Setup Inicial
# Crear proyecto
mkdir calculator-spec-first
cd calculator-spec-first
# Ambiente virtual
python -m venv venv
source venv/bin/activate # Mac/Linux
# venv\Scripts\activate # Windows
# Instalar dependencias
pip install pytest
# Crear estructura
touch calculator.py test_calculator.py
Estructura del Proyecto
calculator-spec-first/
├── calculator.py ← Claude Code implementa aquí
├── test_calculator.py ← Tú escribes los tests aquí
├── requirements.txt ← Solo pytest
└── venv/
requirements.txt:
pytest>=8.0.0
Funcionalidades Obligatorias
1. Operaciones Básicas
Tu calculator debe tener estas funciones con este comportamiento:
add(a, b) — Suma dos números
add(2, 3) # → 5
add(-1, 1) # → 0
add(0.1, 0.2) # → 0.3 (aprox)
subtract(a, b) — Resta b de a
subtract(5, 3) # → 2
subtract(3, 5) # → -2
subtract(0, 0) # → 0
multiply(a, b) — Multiplica dos números
multiply(3, 4) # → 12
multiply(-2, 3) # → -6
multiply(0, 100) # → 0
divide(a, b) — Divide a entre b
divide(10, 2) # → 5.0
divide(7, 2) # → 3.5
divide(10, 0) # → ValueError
2. Manejo de Errores
La calculator debe manejar errores de forma clara:
- División por cero:
ValueErrorcon mensaje "Cannot divide by zero" - Tipos inválidos:
TypeErrorcon mensaje "Arguments must be numbers"
3. Historial de Operaciones
La calculator debe mantener un historial:
calc = Calculator()
calc.add(2, 3) # → 5
calc.multiply(4, 5) # → 20
calc.get_history() # → [{"operation": "add", "args": [2, 3], "result": 5}, ...]
calc.clear_history() # → Limpia historial
calc.get_history() # → []
4. Última Operación
calc = Calculator()
calc.add(2, 3)
calc.get_last_result() # → 5
calc.multiply(4, 5)
calc.get_last_result() # → 20
Paso a Paso: Cómo Construir el Proyecto
Fase A: Tests para Operaciones Básicas
Empieza con los tests más simples. Crea test_calculator.py:
# test_calculator.py
import pytest
from calculator import Calculator
class TestAdd:
"""Spec: add(a, b) returns the sum of two numbers"""
def test_add_positive_numbers(self):
calc = Calculator()
assert calc.add(2, 3) == 5
def test_add_negative_numbers(self):
calc = Calculator()
assert calc.add(-1, -1) == -2
def test_add_positive_and_negative(self):
calc = Calculator()
assert calc.add(-1, 1) == 0
def test_add_zeros(self):
calc = Calculator()
assert calc.add(0, 0) == 0
def test_add_floats(self):
calc = Calculator()
assert calc.add(0.1, 0.2) == pytest.approx(0.3)
def test_add_large_numbers(self):
calc = Calculator()
assert calc.add(1_000_000, 2_000_000) == 3_000_000
class TestSubtract:
"""Spec: subtract(a, b) returns a - b"""
def test_subtract_basic(self):
calc = Calculator()
assert calc.subtract(5, 3) == 2
def test_subtract_result_negative(self):
calc = Calculator()
assert calc.subtract(3, 5) == -2
def test_subtract_zeros(self):
calc = Calculator()
assert calc.subtract(0, 0) == 0
def test_subtract_same_number(self):
calc = Calculator()
assert calc.subtract(42, 42) == 0
def test_subtract_floats(self):
calc = Calculator()
assert calc.subtract(1.5, 0.5) == pytest.approx(1.0)
class TestMultiply:
"""Spec: multiply(a, b) returns a * b"""
def test_multiply_basic(self):
calc = Calculator()
assert calc.multiply(3, 4) == 12
def test_multiply_by_zero(self):
calc = Calculator()
assert calc.multiply(100, 0) == 0
def test_multiply_by_one(self):
calc = Calculator()
assert calc.multiply(42, 1) == 42
def test_multiply_negatives(self):
calc = Calculator()
assert calc.multiply(-2, -3) == 6
def test_multiply_positive_negative(self):
calc = Calculator()
assert calc.multiply(-2, 3) == -6
def test_multiply_floats(self):
calc = Calculator()
assert calc.multiply(2.5, 4) == pytest.approx(10.0)
class TestDivide:
"""Spec: divide(a, b) returns a / b as float"""
def test_divide_even(self):
calc = Calculator()
assert calc.divide(10, 2) == 5.0
def test_divide_with_remainder(self):
calc = Calculator()
assert calc.divide(7, 2) == 3.5
def test_divide_by_one(self):
calc = Calculator()
assert calc.divide(42, 1) == 42.0
def test_divide_zero_numerator(self):
calc = Calculator()
assert calc.divide(0, 5) == 0.0
def test_divide_negative(self):
calc = Calculator()
assert calc.divide(-10, 2) == -5.0
def test_divide_by_zero_raises_error(self):
calc = Calculator()
with pytest.raises(ValueError, match="Cannot divide by zero"):
calc.divide(10, 0)
def test_divide_float_precision(self):
calc = Calculator()
assert calc.divide(1, 3) == pytest.approx(0.333333, rel=1e-4)
Ejecuta pytest (deben fallar porque calculator.py está vacío):
pytest test_calculator.py -v
# ERRORS - ImportError
Da los tests a Claude Code:
Crea calculator.py con una clase Calculator que tenga métodos
add, subtract, multiply, y divide. Los tests en test_calculator.py
definen el comportamiento exacto. Implementa para que todos pasen.
Ejecuta pytest para validar:
pytest test_calculator.py -v
# Debe mostrar 24 passed
Fase B: Tests para Manejo de Errores
Agrega tests para validación de tipos:
class TestTypeValidation:
"""Spec: Calculator raises TypeError for non-numeric inputs"""
def test_add_string_raises_type_error(self):
calc = Calculator()
with pytest.raises(TypeError, match="Arguments must be numbers"):
calc.add("hello", 3)
def test_subtract_none_raises_type_error(self):
calc = Calculator()
with pytest.raises(TypeError, match="Arguments must be numbers"):
calc.subtract(None, 3)
def test_multiply_list_raises_type_error(self):
calc = Calculator()
with pytest.raises(TypeError, match="Arguments must be numbers"):
calc.multiply([1, 2], 3)
def test_divide_bool_raises_type_error(self):
calc = Calculator()
with pytest.raises(TypeError, match="Arguments must be numbers"):
calc.divide(True, 3)
Nota sobre booleans: En Python, bool es subclase de int (True == 1, False == 0). Tu test define que booleans NO son aceptados como números. Esto fuerza a Claude Code a hacer una validación explícita que excluya bool, no solo checar isinstance(x, (int, float)).
Ejecuta pytest — probablemente algunos fallan:
pytest test_calculator.py::TestTypeValidation -v
Si la implementación de Claude Code no maneja tipos, dale el output de pytest y pide corrección. Este es el iteration loop en acción.
Fase C: Tests para Historial
class TestHistory:
"""Spec: Calculator maintains operation history"""
def test_history_starts_empty(self):
calc = Calculator()
assert calc.get_history() == []
def test_add_records_in_history(self):
calc = Calculator()
calc.add(2, 3)
history = calc.get_history()
assert len(history) == 1
assert history[0]["operation"] == "add"
assert history[0]["args"] == [2, 3]
assert history[0]["result"] == 5
def test_multiple_operations_in_history(self):
calc = Calculator()
calc.add(2, 3)
calc.multiply(4, 5)
history = calc.get_history()
assert len(history) == 2
assert history[0]["operation"] == "add"
assert history[1]["operation"] == "multiply"
def test_clear_history(self):
calc = Calculator()
calc.add(1, 1)
calc.subtract(5, 3)
calc.clear_history()
assert calc.get_history() == []
def test_history_after_clear_records_new(self):
calc = Calculator()
calc.add(1, 1)
calc.clear_history()
calc.multiply(3, 3)
history = calc.get_history()
assert len(history) == 1
assert history[0]["operation"] == "multiply"
def test_failed_operation_not_in_history(self):
calc = Calculator()
with pytest.raises(ValueError):
calc.divide(10, 0)
assert calc.get_history() == []
class TestLastResult:
"""Spec: Calculator tracks last operation result"""
def test_last_result_after_add(self):
calc = Calculator()
calc.add(2, 3)
assert calc.get_last_result() == 5
def test_last_result_updates(self):
calc = Calculator()
calc.add(2, 3)
calc.multiply(4, 5)
assert calc.get_last_result() == 20
def test_last_result_none_initially(self):
calc = Calculator()
assert calc.get_last_result() is None
def test_last_result_after_clear_history(self):
calc = Calculator()
calc.add(2, 3)
calc.clear_history()
assert calc.get_last_result() is None
Da los nuevos tests a Claude Code y pide que extienda calculator.py.
Validaciones y Manejo de Errores
Validaciones Obligatorias
- Todos los argumentos numéricos se validan (
intofloat, nobool,str,None, etc.) - División por cero lanza
ValueErrorcon mensaje "Cannot divide by zero" - Tipos inválidos lanzan
TypeErrorcon mensaje "Arguments must be numbers" - Operaciones fallidas NO se registran en historial
Manejo de Errores Esperado
# Tu implementación debe manejar:
calc = Calculator()
# TypeError para tipos inválidos
try:
calc.add("hello", 3)
except TypeError as e:
print(e) # "Arguments must be numbers"
# ValueError para división por cero
try:
calc.divide(10, 0)
except ValueError as e:
print(e) # "Cannot divide by zero"
# Operación fallida no afecta historial
assert calc.get_history() == []
assert calc.get_last_result() is None
Criterios de Éxito
Tu proyecto está completo cuando:
- ✅ Todos los tests pasan (
pytest test_calculator.py -v→ todos green) - ✅ Tienes 25+ tests cubriendo operaciones, errores, historial, y last result
- ✅ Ejecutaste el workflow spec-first: tests primero, implementación después
- ✅ Experimentaste al menos un iteration loop (test falla → Claude Code corrige)
- ✅ La implementación de Claude Code cumple exactamente tu especificación
Rúbrica de Evaluación (100 puntos)
Funcionalidad (50 puntos)
- (15 pts) Las 4 operaciones funcionan correctamente (add, subtract, multiply, divide)
- (10 pts) División por cero lanza ValueError con mensaje correcto
- (10 pts) Tipos inválidos lanzan TypeError con mensaje correcto
- (10 pts) Historial registra operaciones correctamente
- (5 pts) get_last_result funciona y se actualiza correctamente
Tests (30 puntos)
- (10 pts) 25+ tests con buena cobertura
- (5 pts) Tests son deterministas e independientes
- (5 pts) Tests tienen nombres descriptivos
- (5 pts) Edge cases cubiertos (0, negativos, floats, tipos inválidos)
- (5 pts) Tests usan pytest correctamente (assert, pytest.raises, pytest.approx)
Workflow (20 puntos)
- (10 pts) Tests se escribieron ANTES de la implementación (spec-first)
- (5 pts) Se ejecutó al menos un iteration loop (fallo → corrección)
- (5 pts) Código limpio y organizado
Extra Credit (hasta +10 puntos)
- (+5 pts) Tests adicionales para edge cases no listados (números muy grandes, infinity, NaN)
- (+5 pts) Agregar una función extra (power, sqrt, módulo) con tests spec-first
Ejemplo de Implementación Mínima
Este es un esqueleto funcional que muestra la estructura esperada. NO es la solución completa — es el punto de partida que Claude Code debe expandir.
# calculator.py — Esqueleto (NO es la solución completa)
class Calculator:
def __init__(self):
self._history = []
self._last_result = None
def _validate_args(self, a, b):
"""Validate that both arguments are numbers (not bool)."""
# TODO: Implementar validación
pass
def _record(self, operation, args, result):
"""Record operation in history."""
# TODO: Implementar registro
pass
def add(self, a, b):
# TODO: Implementar
pass
def subtract(self, a, b):
# TODO: Implementar
pass
def multiply(self, a, b):
# TODO: Implementar
pass
def divide(self, a, b):
# TODO: Implementar
pass
def get_history(self):
# TODO: Implementar
pass
def clear_history(self):
# TODO: Implementar
pass
def get_last_result(self):
# TODO: Implementar
pass
Este ejemplo:
- ✅ Muestra la estructura esperada (clase con métodos)
- ✅ Indica los métodos que deben existir
- ❌ NO incluye la lógica (eso es lo que Claude Code implementa basándose en tus tests)
Errores Comunes
Error 1: Escribir implementación antes de tests
Causa: Hábito de development sin TDD.
Solución: Abre test_calculator.py primero. No toques calculator.py hasta que tengas tests escritos y ejecutados (fase RED).
Error 2: Tests que no son independientes
Causa: Usar una instancia compartida de Calculator entre tests.
Solución: Cada test crea su propia instancia:
# ❌ Compartida (dependiente)
calc = Calculator() # Al nivel de módulo
def test_add():
calc.add(2, 3) # Modifica historial para otros tests
# ✅ Independiente
def test_add():
calc = Calculator() # Instancia propia
calc.add(2, 3)
Error 3: No testear el manejo de errores
Causa: Enfocarse solo en happy path.
Solución: Los tests de TestTypeValidation y el test de división por cero son obligatorios. Sin ellos, la calculator acepta cualquier input silenciosamente.
Error 4: pytest.approx no usado con floats
Causa: Floats en Python tienen imprecisión inherente.
Solución: Siempre usa pytest.approx al comparar floats:
# ❌ Puede fallar por imprecisión
assert calc.add(0.1, 0.2) == 0.3
# ✅ Correcto
assert calc.add(0.1, 0.2) == pytest.approx(0.3)
Error 5: No dar contexto suficiente a Claude Code
Causa: Solo decir "implementa calculator" sin dar tests.
Solución: Siempre incluye los tests como contexto:
"Implementa calculator.py para que todos los tests en
test_calculator.py pasen. Los tests definen el comportamiento."
Error 6: Boolean como número
Causa: En Python, bool es subclase de int. isinstance(True, int) retorna True.
Solución: Tu test test_divide_bool_raises_type_error fuerza la validación explícita. Si la implementación de Claude Code usa solo isinstance(x, (int, float)), el test de boolean fallará. Dale el output de pytest a Claude Code para que corrija.
# Validación que excluye bool:
def _validate_args(self, a, b):
for arg in (a, b):
if isinstance(arg, bool) or not isinstance(arg, (int, float)):
raise TypeError("Arguments must be numbers")
Proceso Recomendado
Orden sugerido
- Escribe TODOS los tests de la Fase A (operaciones básicas) →
pytest→ todos fallan (RED) - Da tests de Fase A a Claude Code → Claude implementa →
pytest→ debería pasar (GREEN) - Escribe tests de Fase B (errores) →
pytest→ algunos fallan (RED) - Da tests de Fase B a Claude Code → Claude extiende →
pytest→ debería pasar (GREEN) - Escribe tests de Fase C (historial) →
pytest→ fallan (RED) - Da tests de Fase C a Claude Code → Claude extiende →
pytest→ debería pasar (GREEN) - Validación final:
pytest test_calculator.py -v→ 25+ passed
Por qué este orden
Ir fase por fase simula el workflow real de TDD:
- No defines todo de golpe — defines incrementalmente
- Cada fase agrega complejidad sobre lo anterior
- Si algo falla en Fase B, no afecta lo que ya funciona de Fase A
- Experimentas múltiples ciclos RED→GREEN por funcionalidad
Verificación Final
Cuando termines, ejecuta la verificación completa:
# Todos los tests deben pasar
pytest test_calculator.py -v
# Verificar cantidad de tests
pytest test_calculator.py --co -q
# Debe mostrar 25+ tests collected
Output esperado:
test_calculator.py::TestAdd::test_add_positive_numbers PASSED
test_calculator.py::TestAdd::test_add_negative_numbers PASSED
test_calculator.py::TestAdd::test_add_positive_and_negative PASSED
test_calculator.py::TestAdd::test_add_zeros PASSED
test_calculator.py::TestAdd::test_add_floats PASSED
test_calculator.py::TestAdd::test_add_large_numbers PASSED
test_calculator.py::TestSubtract::test_subtract_basic PASSED
...
test_calculator.py::TestHistory::test_failed_operation_not_in_history PASSED
test_calculator.py::TestLastResult::test_last_result_after_add PASSED
test_calculator.py::TestLastResult::test_last_result_updates PASSED
test_calculator.py::TestLastResult::test_last_result_none_initially PASSED
test_calculator.py::TestLastResult::test_last_result_after_clear_history PASSED
========================= 28+ passed in 0.05s =========================
Recursos para el Proyecto
- pytest Documentation - Referencia completa de pytest
- pytest.raises - Cómo testear excepciones
- pytest.approx - Comparaciones de floats con tolerancia
- Python: isinstance - Documentación de isinstance y gotchas con bool
- Anthropic: Claude Code - Documentación oficial de Claude Code
Conexión con Siguiente Módulo
Lo que construiste hoy se expande en el Módulo 2:
- En Módulo 2 aprenderás a generar tests con Claude Code (no solo a escribirlos tú) — prompts que producen tests de calidad profesional
- Los patterns de pytest que usaste aquí (assert, pytest.raises, pytest.approx) se profundizan con parametrize, fixtures, y naming conventions avanzadas
- La calculator que construiste aquí puede servir como base para practicar generación de tests: dale
calculator.pya Claude Code y pide que genere tests adicionales
El workflow spec-first que dominaste aquí es el fundamento de TODA la guía. Cada módulo lo aplica con más complejidad.
Reflexión Final
Antes de avanzar al Módulo 2, reflexiona:
¿Qué experimentaste?
- Escribir tests primero se siente diferente — piensas en comportamiento antes que en implementación
- Los tests eliminan la ambigüedad — Claude Code sabe exactamente qué implementar
- pytest da feedback preciso — no necesitas explicar bugs en lenguaje natural
- El iteration loop es rápido — fallo → feedback → corrección → validación en minutos
¿Qué cambiará en tu workflow?
- Antes: "Claude, implementa X" → review visual → "se ve bien"
- Ahora: escribes tests → "Claude, implementa para que estos tests pasen" → pytest valida → confianza real
Tu superpoder: No eres más lento por escribir tests primero. Eres más rápido — porque no pierdes tiempo debugeando bugs que habrías atrapado con tests. Y eres más confiable — porque pytest valida en 0.05 segundos lo que tomaría 30 minutos de review manual.
Módulo 1, Cápsula 06 — Testing with Claude Code Guide Tu primera app construida con spec-first methodology