En este tema revisaremos errores frecuentes al practicar TDD y cómo corregirlos de manera concreta. Muchos problemas no aparecen porque TDD sea complicado, sino porque se rompe el ritmo rojo, verde y refactor, o porque las pruebas empiezan a comprobar lo incorrecto.
Usaremos ejemplos pequeños en Python con pytest para reconocer síntomas y aplicar
correcciones prácticas.
Un error común es imaginar toda la solución y escribir varias funciones antes de tener una prueba que las pida.
Cuando esto ocurre, TDD deja de guiar el diseño y las pruebas aparecen tarde como verificación posterior.
Elegimos una regla concreta y escribimos una prueba pequeña.
Archivo a crear: tests/test_precios.py
from precios import aplicar_iva
def test_aplica_iva_del_veintiuno_por_ciento():
precio_final = aplicar_iva(100)
assert precio_final == 121
Recién después escribimos el mínimo código necesario.
Archivo a crear: src/precios.py
def aplicar_iva(precio):
return precio * 1.21
Una prueba que mezcla muchas reglas cuesta diagnosticar cuando falla.
Ejemplo a evitar:
def test_compra_completa():
compra = Compra()
compra.agregar("Libro", precio=30, cantidad=2)
compra.agregar("Lápiz", precio=5, cantidad=3)
compra.aplicar_cupon("PROMO10")
compra.confirmar()
assert compra.total == 67.5
assert compra.estado == "confirmada"
assert compra.cantidad_total == 5
assert compra.cupon == "PROMO10"
Esta prueba cubre varias decisiones: ítems, cantidades, cupón y confirmación. Si falla, la causa no es evidente.
Separar la intención mejora el diagnóstico.
def test_agregar_productos_calcula_total():
compra = Compra()
compra.agregar("Libro", precio=30, cantidad=2)
assert compra.total == 60
def test_cupon_descuenta_diez_por_ciento():
compra = Compra()
compra.agregar("Libro", precio=100, cantidad=1)
compra.aplicar_cupon("PROMO10")
assert compra.total == 90
def test_confirmar_compra_cambia_estado():
compra = Compra()
compra.confirmar()
assert compra.estado == "confirmada"
Cada prueba tiene una razón más clara para fallar.
Si una prueba roja requiere muchos cambios para pasar, probablemente el paso fue demasiado grande.
En TDD, una prueba roja debería orientar el siguiente cambio mínimo.
Si la prueba es demasiado amplia, la reemplazamos por un ejemplo más pequeño.
Prueba demasiado ambiciosa:
def test_registra_usuario_valida_datos_guarda_y_envia_correo():
resultado = registrar_usuario({
"nombre": "",
"email": "ana@example.com",
"password": "abc"
})
assert resultado["estado"] == "rechazado"
Primer paso más pequeño:
def test_nombre_es_obligatorio():
errores = validar_datos_registro({
"nombre": "",
"email": "ana@example.com",
"password": "secreto123"
})
assert errores == ["El nombre es obligatorio"]
Una vez resuelta la regla pequeña, podemos volver al flujo completo.
Llegar a verde no significa terminar automáticamente. A veces el código pasa las pruebas, pero quedó duplicado, mal nombrado o difícil de extender.
Ejemplo funcional pero mejorable:
def calcular_descuento(tipo_cliente, total):
if tipo_cliente == "frecuente":
return total * 0.10
if tipo_cliente == "premium":
return total * 0.20
if tipo_cliente == "vip":
return total * 0.30
return 0
El código puede estar en verde, pero la estructura empieza a repetir una misma idea.
Con pruebas pasando, podemos expresar los porcentajes como datos.
PORCENTAJES = {
"frecuente": 0.10,
"premium": 0.20,
"vip": 0.30,
}
def calcular_descuento(tipo_cliente, total):
porcentaje = PORCENTAJES.get(tipo_cliente, 0)
return total * porcentaje
Después del refactor ejecutamos python -m pytest para confirmar que no cambió
el comportamiento.
Si la prueba depende de cómo está implementado el código, los refactors se vuelven costosos.
Ejemplo frágil:
def test_carrito_guarda_items_en_lista_interna():
carrito = Carrito()
carrito.agregar("Libro", precio=30, cantidad=2)
assert carrito._items == [
{"nombre": "Libro", "precio": 30, "cantidad": 2}
]
Esta prueba fallará si cambiamos diccionarios por objetos, aunque el carrito siga funcionando.
Verificamos lo que el carrito promete hacia afuera.
def test_carrito_calcula_total_de_producto_agregado():
carrito = Carrito()
carrito.agregar("Libro", precio=30, cantidad=2)
assert carrito.total == 60
assert carrito.cantidad_de("Libro") == 2
Ahora podemos cambiar la estructura interna sin romper una prueba que describe una regla real.
Los mocks son útiles para dependencias externas, pero pueden volver las pruebas frágiles si reemplazan objetos simples del dominio.
Ejemplo a evitar:
from unittest.mock import Mock
def test_total_de_compra():
item = Mock()
item.subtotal.return_value = 100
compra = Compra([item])
assert compra.total == 100
item.subtotal.assert_called_once()
Si Item es parte del dominio y es fácil de crear, usar un mock agrega ruido.
La prueba queda más clara con objetos reales del dominio.
def test_total_de_compra():
item = Item(nombre="Libro", precio=50, cantidad=2)
compra = Compra([item])
assert compra.total == 100
Reservamos dobles para dependencias lentas, externas o difíciles de controlar.
Pasar el camino feliz no significa que la regla esté completa.
Ejemplo insuficiente:
def test_divide_total_en_cuotas():
assert calcular_cuota(total=100, cuotas=2) == 50
Faltan preguntas importantes: ¿qué pasa con cero cuotas?, ¿con total negativo?, ¿con división que no da exacta?
No hay que probar infinitos casos. Hay que elegir los bordes que cambian la regla.
import pytest
def test_no_permite_cero_cuotas():
with pytest.raises(ValueError, match="Las cuotas deben ser positivas"):
calcular_cuota(total=100, cuotas=0)
def test_redondea_cuota_a_dos_decimales():
assert calcular_cuota(total=100, cuotas=3) == 33.33
Cada borde representa una decisión explícita.
Cuando una prueba falla, puede ser tentador cambiar el valor esperado sin entender qué pasó.
Cambiar expectativas sin esa respuesta reduce la confianza en la suite.
Si cada ejecución tarda demasiado, el ciclo TDD pierde fluidez y empezamos a ejecutar menos pruebas.
Suele pasar cuando mezclamos reglas simples con base de datos, red, navegador o archivos reales en cada prueba.
Durante TDD ejecutamos frecuentemente la parte rápida y dejamos las pruebas más costosas para puntos de integración.
Nombres como test_1, test_ok o test_funciona no ayudan
a entender el comportamiento.
Mejor:
def test_cliente_premium_recibe_veinte_por_ciento_de_descuento():
assert calcular_total(100, tipo_cliente="premium") == 80
El nombre explica la regla incluso antes de leer el cuerpo.
| Error | Corrección |
|---|---|
| Demasiado código antes de probar | Volver al ejemplo mínimo |
| Prueba demasiado grande | Dividir por comportamiento |
| Mucho tiempo en rojo | Reducir el alcance |
| No refactorizar | Mejorar diseño con la suite en verde |
| Probar detalles internos | Probar comportamiento observable |
Revisá esta prueba e identificá al menos tres problemas.
def test_compra():
compra = Compra()
compra._items = []
compra.agregar("Libro", 30, 2)
compra.aplicar_cupon("PROMO10")
compra.confirmar()
assert compra._items[0]["precio"] == 30
assert compra.total == 54
assert compra.estado == "confirmada"
Luego reescribila como pruebas más pequeñas, con nombres claros y sin acceder a atributos internos.
Practicar TDD requiere ajustar el ritmo. Los errores más comunes se corrigen volviendo a pasos pequeños, nombres claros, pruebas enfocadas y refactor seguro. Cuando una prueba falla, debe ayudarnos a entender mejor el sistema, no agregar confusión.
En el próximo tema veremos cómo sostener el ritmo de trabajo con commits pequeños, pruebas rápidas y retroalimentación constante.