31. Errores frecuentes al practicar TDD y cómo corregirlos

31.1 Objetivo del tema

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.

31.2 Error 1: escribir demasiado código antes de la prueba

Un error común es imaginar toda la solución y escribir varias funciones antes de tener una prueba que las pida.

Síntoma: el código crece rápido, pero no queda claro qué prueba justifica cada parte.

Cuando esto ocurre, TDD deja de guiar el diseño y las pruebas aparecen tarde como verificación posterior.

31.3 Corrección: volver al ejemplo mínimo

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

31.4 Error 2: escribir una prueba demasiado grande

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.

31.5 Corrección: dividir por comportamiento

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.

31.6 Error 3: quedarse demasiado tiempo en rojo

Si una prueba roja requiere muchos cambios para pasar, probablemente el paso fue demasiado grande.

Síntoma: pasan varios minutos y todavía no sabemos si el fallo actual corresponde al requisito, a un error de diseño o a un problema de implementación.

En TDD, una prueba roja debería orientar el siguiente cambio mínimo.

31.7 Corrección: reducir el alcance

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.

31.8 Error 4: no refactorizar después del verde

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.

31.9 Corrección: refactor pequeño y seguro

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.

31.10 Error 5: probar detalles internos

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.

31.11 Corrección: probar comportamiento observable

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.

31.12 Error 6: usar mocks para todo

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.

31.13 Corrección: usar objetos reales cuando son simples

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.

31.14 Error 7: ignorar casos borde

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?

31.15 Corrección: agregar bordes relevantes

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.

31.16 Error 8: cambiar pruebas para ocultar fallos

Cuando una prueba falla, puede ser tentador cambiar el valor esperado sin entender qué pasó.

Si una prueba falla, primero preguntamos: ¿cambió una regla del negocio o rompimos el comportamiento esperado?

Cambiar expectativas sin esa respuesta reduce la confianza en la suite.

31.17 Corrección: investigar antes de ajustar

  1. Leer el nombre de la prueba que falló.
  2. Confirmar qué regla estaba protegiendo.
  3. Revisar el cambio reciente que pudo afectarla.
  4. Si la regla cambió, actualizar la prueba y el código de forma consciente.
  5. Si la regla no cambió, corregir la implementación.

31.18 Error 9: pruebas lentas en el ciclo principal

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.

31.19 Corrección: separar tipos de pruebas

  • Pruebas de dominio rápidas para reglas principales.
  • Pruebas de adaptadores para entrada y salida.
  • Pruebas de integración para verificar conexiones reales.
  • Pocas pruebas end-to-end para flujos críticos.

Durante TDD ejecutamos frecuentemente la parte rápida y dejamos las pruebas más costosas para puntos de integración.

31.20 Error 10: no nombrar bien las pruebas

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.

31.21 Resumen de correcciones

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

31.22 Ejercicio práctico

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.

31.23 Checklist del tema

  • La prueba roja debe ser pequeña y clara.
  • El código verde debe ser suficiente, no anticipado.
  • El refactor se realiza con la suite pasando.
  • Las pruebas verifican comportamiento observable.
  • Los bordes importantes quedan expresados como ejemplos ejecutables.

31.24 Conclusión

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.