Módulo 4: TDD Workflow Completo
Refactoring con Tests como Safety Net
Refactoring con Tests como Safety Net
Descripción de la cápsula
Cuando Claude Code implementa código que pasa tus tests, el ciclo TDD llega a verde. ¿Qué sigue? Refactorizar. Mejorar la estructura, el naming, eliminar duplicación — sin cambiar el comportamiento. Sin tests, cada refactoring es un salto al vacío: puedes romper algo sin saberlo hasta que un usuario reporte un bug en producción. Con tests verdes, tienes una red de seguridad: si el refactoring cambia el comportamiento, los tests fallan de inmediato. Esta cápsula te enseña a usar los tests como safety net para refactorizar con confianza — y a guiar a Claude Code para que lo haga también.
¿Qué es el refactoring?
Definición: estructura sin cambio de comportamiento
Refactoring es cambiar la estructura interna del código sin cambiar su comportamiento observable. El programa hace exactamente lo mismo antes y después: mismas entradas, mismas salidas, mismos efectos secundarios. Lo único que cambia es cómo está organizado el código — legibilidad, modularidad, naming, eliminación de duplicación.
Antes del refactoring: input X → output Y
Después del refactoring: input X → output Y (idéntico)
Si output cambia → NO es refactoring, es un cambio de feature o un bug.
Los tests definen el "comportamiento"
En TDD, los tests son la especificación del comportamiento. Si los tests pasan antes del refactoring y siguen pasando después, el comportamiento no cambió — el refactoring es seguro.
Sin tests: "¿Cambié algo sin querer?" → No sabes hasta que alguien lo note
Con tests: pytest dice PASS → comportamiento preservado
Regla de oro: Sin tests, refactorizar es apostar. Con tests, refactorizar es ingeniería.
La fase de refactoring en TDD agentic
Dónde encaja en el ciclo Red-Green-Refactor
1. RED → Escribes un test que falla
2. GREEN → Claude Code implementa hasta que el test pasa
3. REFACTOR → Mejoras el código (esta cápsula)
4. Repites
La fase de refactoring ocurre solo cuando todos los tests pasan. Si hay algún test rojo, no refactorices — primero pon verde.
El workflow con Claude Code
Caso ideal:
Tú: "Refactoriza este código manteniendo todos los tests pasando. Mejora: [naming, estructura, DRY, etc.]"
Claude Code refactoriza
Tú: ejecutas pytest
Resultado: todos los tests pasan → refactoring exitoso
Caso donde Claude Code introduce un bug:
Claude Code refactoriza
Tú: ejecutas pytest
Resultado: un test falla
Tú: revert (git checkout o undo)
Tú: intentas un enfoque diferente o ajustas el prompt
El safety net funciona en ambos sentidos: protege tu código y te dice cuándo Claude Code se equivocó.
Patrones comunes de refactoring con Claude Code
a) Extraer función (Extract Function)
Código monolítico en una sola función se divide en funciones más pequeñas con responsabilidades claras.
Antes:
# order_service.py
def process_order(order: dict) -> dict:
# Validación (15 líneas)
if "items" not in order or not order["items"]:
raise ValueError("Order must have items")
for item in order["items"]:
if "price" not in item or "qty" not in item:
raise ValueError("Each item must have price and qty")
if item["price"] < 0 or item["qty"] < 1:
raise ValueError("Invalid price or quantity")
# Cálculo (10 líneas)
subtotal = sum(item["price"] * item["qty"] for item in order["items"])
tax_rate = 0.16
tax = subtotal * tax_rate
total = subtotal + tax
# Formato (8 líneas)
return {
"order_id": order.get("order_id", "N/A"),
"subtotal": round(subtotal, 2),
"tax": round(tax, 2),
"total": round(total, 2),
}
Después del refactoring:
# order_service.py
def validate_order(order: dict) -> None:
"""Valida que el order tenga items válidos. Raises ValueError si no."""
if "items" not in order or not order["items"]:
raise ValueError("Order must have items")
for item in order["items"]:
if "price" not in item or "qty" not in item:
raise ValueError("Each item must have price and qty")
if item["price"] < 0 or item["qty"] < 1:
raise ValueError("Invalid price or quantity")
def calculate_total(items: list[dict], tax_rate: float = 0.16) -> tuple[float, float, float]:
"""Retorna (subtotal, tax, total)."""
subtotal = sum(item["price"] * item["qty"] for item in items)
tax = subtotal * tax_rate
total = subtotal + tax
return subtotal, tax, total
def format_receipt(order: dict, subtotal: float, tax: float, total: float) -> dict:
"""Formatea el recibo con valores redondeados."""
return {
"order_id": order.get("order_id", "N/A"),
"subtotal": round(subtotal, 2),
"tax": round(tax, 2),
"total": round(total, 2),
}
def process_order(order: dict) -> dict:
validate_order(order)
subtotal, tax, total = calculate_total(order["items"])
return format_receipt(order, subtotal, tax, total)
Los tests siguen pasando porque process_order sigue produciendo el mismo output para los mismos inputs.
b) Renombrar para claridad
Variables y funciones con nombres vagos (data, x, do_stuff) se renombran para que el código se lea como prosa.
# Antes
def calc(o):
t = sum(i["p"] * i["q"] for i in o["items"])
return t * 1.16
# Después
def calculate_order_total(order: dict) -> float:
subtotal = sum(item["price"] * item["quantity"] for item in order["items"])
return subtotal * 1.16
c) Eliminar duplicación (DRY)
Código repetido se extrae a una función o constante compartida.
# Antes: duplicación
def get_user_email(user):
if not user or "email" not in user or not user["email"]:
raise ValueError("Invalid user")
return user["email"]
def get_user_name(user):
if not user or "name" not in user or not user["name"]:
raise ValueError("Invalid user")
return user["name"]
# Después: DRY
def get_required_field(obj: dict, field: str) -> str:
if not obj or field not in obj or not obj[field]:
raise ValueError("Invalid user")
return obj[field]
def get_user_email(user: dict) -> str:
return get_required_field(user, "email")
def get_user_name(user: dict) -> str:
return get_required_field(user, "name")
d) Simplificar condicionales
Condiciones complejas se extraen a funciones con nombres descriptivos o se reestructuran.
# Antes
def can_access(user, resource):
if user and user.get("active") and (user.get("role") == "admin" or (user.get("role") == "editor" and resource.get("owner") == user.get("id"))):
return True
return False
# Después
def is_admin(user: dict) -> bool:
return user.get("role") == "admin"
def is_owner(user: dict, resource: dict) -> bool:
return user.get("role") == "editor" and resource.get("owner") == user.get("id")
def can_access(user: dict | None, resource: dict) -> bool:
if not user or not user.get("active"):
return False
return is_admin(user) or is_owner(user, resource)
e) Añadir type hints
Mejorar la documentación del código sin cambiar la lógica.
# Antes
def merge(a, b):
return {**a, **b}
# Después
def merge(a: dict[str, object], b: dict[str, object]) -> dict[str, object]:
return {**a, **b}
El protocolo de seguridad del refactoring
Sigue estos pasos para que cada refactoring sea seguro:
1. ¿Todos los tests pasan? → NO → Arregla primero, luego refactoriza
→ SÍ → Continúa
2. Haz UN SOLO cambio de refactoring
3. Ejecuta los tests
4. ¿Siguen pasando todos?
├── SÍ → commit, siguiente refactoring (o termina)
└── NO → revert, entiende por qué falló, intenta de otra forma
Reglas clave:
- ✅ Un cambio a la vez — si fallan los tests, sabes exactamente qué lo causó
- ✅ Commit después de cada refactoring exitoso — punto de retorno claro
- ❌ No refactorices y añadas features en el mismo commit
- ❌ No hagas múltiples refactorings sin ejecutar tests entre medias
Flujo en la terminal
# 1. Verificar que todo pasa
pytest -v
# 2. Hacer un solo cambio de refactoring (o pedirle a Claude Code que lo haga)
# 3. Ejecutar tests de nuevo
pytest -v
# 4a. Si pasan: commit
git add .
git commit -m "refactor: extrae validate_email en auth_service"
# 4b. Si fallan: revert
git checkout -- auth_service.py
# Luego: analiza por qué falló, ajusta el enfoque
Cómo pedirle refactoring a Claude Code
Prompts efectivos
General:
Refactoriza [función/archivo] para mejorar legibilidad. Los tests deben seguir pasando.
Extracción de lógica:
Extrae la lógica de validación en una función separada. Mantén todos los tests verdes.
DRY:
Elimina la duplicación entre [func_a] y [func_b]. Crea una función compartida.
Naming:
Renombra las variables y funciones para mayor claridad. No cambies el comportamiento.
Con contexto:
Refactoriza process_order. Los tests en test_order_service.py definen el contrato.
Mejora: extrae validación, cálculo y formato en funciones separadas.
Ejemplo real de interacción
Antes (código funcional pero desordenado):
# auth.py
def register(u):
if not u.get("email") or "@" not in u["email"]:
raise ValueError("bad email")
if not u.get("password") or len(u["password"]) < 8:
raise ValueError("bad password")
h = hashlib.sha256(u["password"].encode()).hexdigest()
return {"email": u["email"], "hash": h}
Prompt a Claude Code:
Refactoriza la función register en auth.py:
1. Extrae la validación del email en validate_email
2. Extrae la validación del password en validate_password
3. Extrae el hashing en hash_password
4. Añade type hints
Los tests en test_auth.py deben seguir pasando. No cambies el comportamiento.
Después (Claude Code refactoriza):
# auth.py
import hashlib
def validate_email(user: dict) -> None:
if not user.get("email") or "@" not in user["email"]:
raise ValueError("bad email")
def validate_password(user: dict) -> None:
if not user.get("password") or len(user["password"]) < 8:
raise ValueError("bad password")
def hash_password(password: str) -> str:
return hashlib.sha256(password.encode()).hexdigest()
def register(user: dict) -> dict:
validate_email(user)
validate_password(user)
hashed = hash_password(user["password"])
return {"email": user["email"], "hash": hashed}
Ejecutas pytest test_auth.py -v → todos pasan → refactoring seguro.
Cuándo NO refactorizar
Hay situaciones donde refactorizar es contraproducente:
1. Los tests son verdes pero frágiles
Si los tests dependen de detalles de implementación (nombres de funciones internas, orden de llamadas), se romperán al refactorizar aunque el comportamiento sea correcto. Primero refactoriza los tests para que verifiquen solo inputs y outputs.
2. Vas a añadir una nueva feature
No refactorices en el mismo paso que implementas una feature. Flujo correcto:
- Escribe tests para la nueva feature (rojo)
- Implementa (verde)
- Refactoriza (mantén verde)
3. El "refactoring" cambia el comportamiento
Si tu cambio modifica outputs, manejo de errores o edge cases, no es refactoring — es una nueva feature o un bug. Escribe tests para el nuevo comportamiento y trata el cambio como tal.
4. No tienes tests para esa parte del código
Sin tests, no tienes safety net. Primero escribe tests (aunque sea test-after para código legacy) y luego refactoriza.
Checklist antes de refactorizar
Usa esta lista mental antes de cada sesión de refactoring:
- ✅ Todos los tests pasan
- ✅ El código está bajo control de versiones (puedes revertir)
- ✅ Sabes qué cambio vas a hacer (uno solo)
- ✅ Los tests verifican comportamiento, no implementación
- ❌ No hay features en curso que dependan de ese código sin testear
Ejemplo práctico completo: refactoring de registro de usuario
Partimos de una función que funciona pero está desordenada. Los tests ya pasan.
Código inicial (funcional pero monolítico)
# auth_service.py
import hashlib
import re
def register_user(data):
# Todo en una sola función
if not data:
raise ValueError("data required")
email = data.get("email", "").strip().lower()
if not email or not re.match(r"^[\w\.-]+@[\w\.-]+\.\w+$", email):
raise ValueError("invalid email")
pwd = data.get("password", "")
if len(pwd) < 8:
raise ValueError("password must be at least 8 chars")
if not any(c.isupper() for c in pwd) or not any(c.isdigit() for c in pwd):
raise ValueError("password needs uppercase and digit")
salt = "auth_salt_v1"
hashed = hashlib.sha256((salt + pwd).encode()).hexdigest()
return {"email": email, "password_hash": hashed}
# test_auth_service.py
import pytest
from auth_service import register_user
def test_register_valid_email():
result = register_user({"email": "user@test.com", "password": "SecurePass1"})
assert result["email"] == "user@test.com"
assert "password_hash" in result
assert len(result["password_hash"]) == 64
def test_register_invalid_email_raises():
with pytest.raises(ValueError, match="invalid email"):
register_user({"email": "bad", "password": "SecurePass1"})
def test_register_weak_password_raises():
with pytest.raises(ValueError, match="password must be at least 8 chars"):
register_user({"email": "u@t.com", "password": "short"})
def test_register_password_needs_uppercase_and_digit():
with pytest.raises(ValueError, match="password needs uppercase and digit"):
register_user({"email": "u@t.com", "password": "lowercase1"})
pytest test_auth_service.py -v → 4 passed.
Paso 1: Extraer validación de email
Prompt: "Extrae la validación del email en una función validate_email. Los tests deben seguir pasando."
# auth_service.py (después del paso 1)
import hashlib
import re
def validate_email(data: dict) -> str:
if not data:
raise ValueError("data required")
email = data.get("email", "").strip().lower()
if not email or not re.match(r"^[\w\.-]+@[\w\.-]+\.\w+$", email):
raise ValueError("invalid email")
return email
def register_user(data: dict) -> dict:
email = validate_email(data)
pwd = data.get("password", "")
if len(pwd) < 8:
raise ValueError("password must be at least 8 chars")
if not any(c.isupper() for c in pwd) or not any(c.isdigit() for c in pwd):
raise ValueError("password needs uppercase and digit")
salt = "auth_salt_v1"
hashed = hashlib.sha256((salt + pwd).encode()).hexdigest()
return {"email": email, "password_hash": hashed}
pytest test_auth_service.py -v → 4 passed.
Paso 2: Extraer validación de password
Prompt: "Extrae la validación del password en validate_password. Mantén los tests verdes."
def validate_password(data: dict) -> str:
pwd = data.get("password", "")
if len(pwd) < 8:
raise ValueError("password must be at least 8 chars")
if not any(c.isupper() for c in pwd) or not any(c.isdigit() for c in pwd):
raise ValueError("password needs uppercase and digit")
return pwd
def register_user(data: dict) -> dict:
email = validate_email(data)
pwd = validate_password(data)
salt = "auth_salt_v1"
hashed = hashlib.sha256((salt + pwd).encode()).hexdigest()
return {"email": email, "password_hash": hashed}
pytest → 4 passed.
Paso 3: Extraer hashing
Prompt: "Extrae el hashing en una función hash_password. Los tests no deben cambiar."
def hash_password(password: str, salt: str = "auth_salt_v1") -> str:
return hashlib.sha256((salt + password).encode()).hexdigest()
def register_user(data: dict) -> dict:
email = validate_email(data)
pwd = validate_password(data)
hashed = hash_password(pwd)
return {"email": email, "password_hash": hashed}
pytest → 4 passed.
Paso 4: Renombrar y type hints
Prompt: "Renombra pwd a password y añade type hints completos. No cambies el comportamiento."
def register_user(data: dict) -> dict:
email = validate_email(data)
password = validate_password(data)
hashed = hash_password(password)
return {"email": email, "password_hash": hashed}
pytest → 4 passed.
Cada paso fue un cambio pequeño. Cada paso mantuvo los tests verdes. El código final es más legible y mantenible.
Código ejecutable completo del ejemplo
Para que puedas ejecutarlo localmente:
# auth_service.py (versión final refactorizada)
import hashlib
import re
def validate_email(data: dict) -> str:
if not data:
raise ValueError("data required")
email = data.get("email", "").strip().lower()
if not email or not re.match(r"^[\w\.-]+@[\w\.-]+\.\w+$", email):
raise ValueError("invalid email")
return email
def validate_password(data: dict) -> str:
pwd = data.get("password", "")
if len(pwd) < 8:
raise ValueError("password must be at least 8 chars")
if not any(c.isupper() for c in pwd) or not any(c.isdigit() for c in pwd):
raise ValueError("password needs uppercase and digit")
return pwd
def hash_password(password: str, salt: str = "auth_salt_v1") -> str:
return hashlib.sha256((salt + password).encode()).hexdigest()
def register_user(data: dict) -> dict:
email = validate_email(data)
password = validate_password(data)
hashed = hash_password(password)
return {"email": email, "password_hash": hashed}
# test_auth_service.py
import pytest
from auth_service import register_user, validate_email, validate_password, hash_password
def test_register_valid_email():
result = register_user({"email": "user@test.com", "password": "SecurePass1"})
assert result["email"] == "user@test.com"
assert "password_hash" in result
assert len(result["password_hash"]) == 64
def test_register_invalid_email_raises():
with pytest.raises(ValueError, match="invalid email"):
register_user({"email": "bad", "password": "SecurePass1"})
def test_register_weak_password_raises():
with pytest.raises(ValueError, match="password must be at least 8 chars"):
register_user({"email": "u@t.com", "password": "short"})
def test_register_password_needs_uppercase_and_digit():
with pytest.raises(ValueError, match="password needs uppercase and digit"):
register_user({"email": "u@t.com", "password": "lowercase1"})
def test_validate_email_extracted():
assert validate_email({"email": " User@Test.COM "}) == "user@test.com"
def test_hash_password_deterministic():
h1 = hash_password("SecurePass1")
h2 = hash_password("SecurePass1")
assert h1 == h2
pytest test_auth_service.py -v
# 6 passed
Troubleshooting
Problema 1: Los tests fallan después del refactoring de Claude Code
Causa: Claude Code cambió el comportamiento sin querer, o los tests acoplan a la implementación.
Solución: Revisa el diff. Si Claude Code cambió la lógica (no solo la estructura), revierte y pide: "Refactoriza manteniendo el comportamiento exacto. No cambies las condiciones ni los cálculos." Si los tests verifican implementación (ej. que exista una función con cierto nombre interno), refactoriza los tests para que verifiquen solo el contrato (inputs/outputs).
Problema 2: Claude Code hace varios refactorings a la vez y no sé cuál rompió los tests
Causa: Un solo prompt pidiendo muchos cambios.
Solución: Pide un refactoring a la vez. "Extrae solo la validación del email. Luego te pediré el siguiente paso."
Problema 3: Los tests pasan pero el código refactorizado tiene bugs sutiles
Causa: Los tests no cubren ese caso. El refactoring introdujo un edge case no testeado.
Solución: Añade un test para el caso que falla, luego corrige el código. El refactoring reveló un gap en la cobertura — úsalo como oportunidad para mejorar los tests.
Problema 4: No sé qué refactoring hacer primero
Causa: El código tiene múltiples deudas técnicas.
Solución: Prioriza por impacto en legibilidad. Empieza con: (1) funciones muy largas (extraer), (2) duplicación obvia (DRY), (3) nombres confusos (renombrar). Un cambio a la vez, tests después de cada uno.
Problema 5: Claude Code "refactoriza" y cambia la API pública
Causa: El prompt no fue explícito sobre no cambiar interfaces.
Solución: Incluye en el prompt: "No cambies las firmas de las funciones públicas. Los tests importan y llaman a las mismas funciones con los mismos argumentos."
Conexión con Proyecto
En el proyecto de autenticación del Módulo 4 (Feature completa con TDD), cada ciclo TDD termina con refactoring:
Ciclo 1: test_register_validates_email → implementación → refactoring (extraer validate_email)
Ciclo 2: test_register_hashes_password → implementación → refactoring (extraer hash_password)
Ciclo 3: test_register_creates_user → implementación → refactoring (ordenar dependencias)
...
El refactoring después de cada ciclo verde mantiene el código limpio mientras construyes la feature. Sin refactoring entre ciclos, al final tendrías un montón de código que funciona pero es imposible de mantener. Con refactoring constante, entregas código que funciona y es legible.
Ejercicios
Ejercicio 1: Identificar qué es refactoring (Fácil)
Clasifica cada cambio como refactoring (sí/no) y justifica:
a) Cambiar x + x por 2 * x en una función de cálculo
b) Cambiar el mensaje de un ValueError de "invalid" a "Invalid input"
c) Extraer 20 líneas de validación a una función validate_input
d) Cambiar un if por match/case manteniendo la misma lógica
e) Eliminar una comprobación de None porque "nunca ocurre"
Ver solución
- a) Sí. Misma salida para misma entrada. Cambio puramente estructural/matemático.
- b) Depende. Si los tests verifican el mensaje exacto con
match="invalid", ya no es refactoring — cambias el contrato. Si los tests solo verifican que se lanzaValueError, sí es refactoring. - c) Sí. Estructura cambia, comportamiento no.
- d) Sí. Misma lógica, sintaxis distinta.
- e) No. Cambias el comportamiento (el código ya no maneja
None). Es un cambio de feature o un posible bug.
Ejercicio 2: Extraer función con Claude Code (Fácil)
Tienes esta función:
def calculate_shipping(cart):
total = sum(item["price"] * item["qty"] for item in cart["items"])
if total >= 100:
return 0
if cart.get("country") == "MX":
return 50
return 100
Escribe el prompt que darías a Claude Code para extraer el cálculo del costo de envío en una función get_shipping_cost(total, country).
Ver solución
Refactoriza calculate_shipping: extrae la lógica del costo de envío en una función
get_shipping_cost(total: float, country: str | None) -> float.
Reglas:
- total >= 100 → 0
- country == "MX" → 50
- resto → 100
calculate_shipping debe llamar a get_shipping_cost después de calcular el total.
No cambies el comportamiento. Los tests en test_shipping.py deben seguir pasando.
Ejercicio 3: Protocolo de seguridad (Medio)
Tienes 3 refactorings pendientes en order_processor.py:
- Renombrar
calcacalculate_total - Extraer validación a
validate_order - Añadir type hints
¿En qué orden los harías y por qué?
Ver solución
Orden sugerido:
- Extraer validación primero — es el cambio más grande y el que más riesgo tiene de romper algo. Si falla, lo detectas pronto.
- Renombrar después — es seguro si los tests no acoplan al nombre de la función (los tests importan por nombre de módulo/función, que puede cambiar con un rename).
- Type hints al final — es puramente aditivo, no cambia el comportamiento.
Alternativa: si calc es una función interna que los tests no llaman directamente, el rename puede ser primero. La regla: el cambio más invasivo primero (cuando los tests están verdes), para tener feedback rápido.
Ejercicio 4: Refactoring paso a paso (Medio)
Código inicial:
def process(items):
r = []
for i in items:
if i.get("active"):
r.append({"id": i["id"], "name": i["name"].upper()})
return r
Haz 3 refactorings en orden, ejecutando tests hipotéticos después de cada uno. Los tests verifican: para [{"id": 1, "name": "a", "active": True}] → [{"id": 1, "name": "A"}].
Ver solución
Paso 1 — Renombrar variables:
def process(items: list[dict]) -> list[dict]:
result = []
for item in items:
if item.get("active"):
result.append({"id": item["id"], "name": item["name"].upper()})
return result
Tests: pass.
Paso 2 — Extraer transformación de item:
def format_active_item(item: dict) -> dict:
return {"id": item["id"], "name": item["name"].upper()}
def process(items: list[dict]) -> list[dict]:
result = []
for item in items:
if item.get("active"):
result.append(format_active_item(item))
return result
Tests: pass.
Paso 3 — List comprehension (opcional, más idiomático):
def format_active_item(item: dict) -> dict:
return {"id": item["id"], "name": item["name"].upper()}
def process(items: list[dict]) -> list[dict]:
return [format_active_item(item) for item in items if item.get("active")]
Tests: pass.
Ejercicio 5: Cuándo NO refactorizar (Medio)
Tienes tests verdes pero sabes que:
- El test
test_loginusamock.patch("auth.hash_password")para mockear la función interna - El test
test_registerverifica queUserRepository.createse llama exactamente una vez
¿Qué harías antes de refactorizar y por qué?
Ver solución
Problema: Los tests acoplan a la implementación (nombre de función interna, número de llamadas a un método concreto). Cualquier refactoring que renombre hash_password, la extraiga a otro módulo o cambie cómo se usa UserRepository romperá los tests aunque el comportamiento sea correcto.
Acción: Refactoriza los tests primero para que sean de caja negra:
test_login: dado un password, el resultado debe tener un hash (sin importar qué función lo calcule). O usa un hash conocido y verifica el output.test_register: dado un usuario válido, verifica que exista en la base (o en el repo) con los datos correctos, sin importar cuántas veces se llamó acreate.
Después de que los tests verifiquen comportamiento y no implementación, refactoriza el código con confianza.
Ejercicio 6: Prompt completo para Claude Code (Medio)
Tienes payment_processor.py con una función process_payment de 80 líneas que valida, calcula impuestos, aplica descuentos y guarda en DB. Escribe un prompt completo para que Claude Code la refactorice en pasos seguros.
Ver solución
Refactoriza process_payment en payment_processor.py.
Objetivo: dividir en funciones más pequeñas sin cambiar el comportamiento.
Pasos que quiero (haz solo el primero por ahora):
1. Extrae la validación del payment (tarjeta, monto, etc.) en validate_payment(payment: dict) -> None
2. Después te pediré el siguiente paso
Reglas:
- Los tests en test_payment_processor.py definen el contrato. Deben seguir pasando.
- No cambies las excepciones ni los mensajes de error.
- Mantén las mismas firmas públicas (process_payment debe seguir existiendo con los mismos parámetros).
- Añade type hints a las funciones nuevas.
Explicación: Pedir "solo el primer paso" evita que Claude Code haga demasiados cambios a la vez. Si algo falla, sabes que fue la extracción de validación.
Resumen
- ✅ Refactoring = cambiar estructura sin cambiar comportamiento
- ✅ Los tests definen el comportamiento — si pasan antes y después, el refactoring es seguro
- ✅ Sin tests, refactorizar es riesgo; con tests, es ingeniería
- ✅ Fase REFACTOR solo cuando todos los tests están verdes
- ✅ Un cambio a la vez, tests después de cada uno, commit si pasan
- ✅ Patrones útiles: extraer función, renombrar, DRY, simplificar condicionales, type hints
- ✅ Prompts claros a Claude Code: "Refactoriza X. Los tests deben seguir pasando."
- ❌ No refactorices si los tests son frágiles (refactoriza los tests primero)
- ❌ No mezcles refactoring con nuevas features en el mismo paso
Recursos Adicionales
- Martin Fowler: Refactoring — Catálogo clásico de refactorings con ejemplos
- Test-Driven Development by Example (Kent Beck) — El libro que popularizó Red-Green-Refactor
- Refactoring Guru — Patrones de refactoring con ejemplos visuales
- Python Type Hints (PEP 484) — Guía oficial de type hints en Python
- Working Effectively with Legacy Code (Michael Feathers) — Cómo añadir tests a código existente antes de refactorizar
- Clean Code (Robert C. Martin) — Principios de código limpio aplicables al refactoring
Módulo 4, Cápsula 05 — Testing with Claude Code Guide