40. Errores frecuentes al escribir pruebas unitarias

40.1 Introducción

Escribir pruebas unitarias no consiste solo en agregar archivos de test. Una prueba puede existir, ejecutarse y pasar, pero aun así aportar poco valor si no verifica nada importante, si es difícil de leer o si se rompe por detalles internos irrelevantes.

En este tema reunimos errores frecuentes que aparecen al comenzar y también en suites maduras que no recibieron mantenimiento. Muchos de estos problemas ya fueron mencionados en temas anteriores, pero aquí los veremos juntos para reconocerlos rápidamente.

El objetivo no es buscar pruebas perfectas. El objetivo es escribir pruebas útiles: claras, rápidas, independientes y capaces de detectar regresiones reales.

40.2 Error 1: escribir pruebas sin aserciones

Una prueba sin aserciones suele ejecutar código, pero no comprueba el comportamiento esperado. Puede pasar aunque la unidad produzca un resultado incorrecto.

def test_calcular_total():
    calcular_total(1000, 21)

Esta prueba solo llama a la función. Una versión útil expresa una expectativa:

def test_calcular_total_con_impuesto_del_21_por_ciento():
    assert calcular_total(1000, 21) == 1210

La aserción es lo que permite que la prueba detecte un error en el cálculo.

40.3 Error 2: nombres demasiado genéricos

Un nombre como test_usuario, test_total o test_validacion no explica qué comportamiento se verifica. Cuando falla, obliga a leer todo el cuerpo de la prueba para entender el problema.

def test_usuario():
    usuario = Usuario(edad=18)

    assert usuario.puede_registrarse() is True

Un nombre más claro documenta la regla:

def test_usuario_de_18_anios_puede_registrarse():
    usuario = Usuario(edad=18)

    assert usuario.puede_registrarse() is True

El nombre debe ayudar incluso antes de mirar la implementación.

40.4 Error 3: probar demasiadas cosas en una sola prueba

Una prueba con muchas responsabilidades es difícil de diagnosticar. Si falla, no queda claro qué comportamiento se rompió.

def test_carrito():
    carrito = Carrito()
    assert carrito.total() == 0
    carrito.agregar("mouse", 1000)
    assert carrito.cantidad() == 1
    assert carrito.total() == 1000
    carrito.vaciar()
    assert carrito.total() == 0

Conviene separar comportamientos:

def test_carrito_vacio_tiene_total_cero():
    carrito = Carrito()

    assert carrito.total() == 0


def test_agregar_producto_actualiza_cantidad_y_total():
    carrito = Carrito()

    carrito.agregar("mouse", 1000)

    assert carrito.cantidad() == 1
    assert carrito.total() == 1000

Cada prueba debe tener una intención principal.

40.5 Error 4: depender del orden de ejecución

Las pruebas unitarias deben ser independientes. Si una prueba necesita que otra se ejecute antes, la suite se vuelve frágil.

contador = Contador()


def test_incrementar():
    contador.incrementar()

    assert contador.valor == 1


def test_incrementar_otra_vez():
    contador.incrementar()

    assert contador.valor == 2

La segunda prueba depende de que la primera haya modificado el contador. Una versión correcta crea estado propio:

def test_incrementar_aumenta_el_valor_en_uno():
    contador = Contador()

    contador.incrementar()

    assert contador.valor == 1

Cada prueba debe poder ejecutarse sola y producir el mismo resultado.

40.6 Error 5: compartir estado mutable

Compartir listas, diccionarios u objetos mutables entre pruebas puede causar contaminación. Una prueba modifica el objeto y otra recibe un estado inesperado.

pedido_base = {"cliente": "Ana", "items": ["mouse"], "total": 1000}


def test_pedido_sin_items_no_puede_confirmarse():
    pedido_base["items"] = []

    assert puede_confirmarse(pedido_base) is False

Es mejor crear una instancia nueva para cada prueba:

def crear_pedido(items=None):
    if items is None:
        items = ["mouse"]
    return {"cliente": "Ana", "items": items, "total": 1000}


def test_pedido_sin_items_no_puede_confirmarse():
    pedido = crear_pedido(items=[])

    assert puede_confirmarse(pedido) is False

La independencia evita fallas intermitentes y difíciles de diagnosticar.

40.7 Error 6: usar datos de prueba confusos

Los datos de prueba deben ayudar a entender el caso. Si elegimos números difíciles sin necesidad, la prueba se vuelve menos legible.

def test_descuento():
    assert calcular_descuento(3789.45, 15) == 3221.0325

Si el objetivo es comprobar un descuento del 15%, un dato simple comunica mejor:

def test_descuento_del_15_por_ciento():
    assert calcular_descuento(1000, 15) == 850

Usa datos complejos solo cuando el caso realmente lo necesita.

40.8 Error 7: no probar casos límite

Muchos errores aparecen en los bordes de una regla. Probar solo un valor cómodo puede dejar sin cobertura el comportamiento más importante.

def test_puede_registrarse():
    assert puede_registrarse(25) is True

Si la regla depende de la mayoría de edad, conviene probar alrededor del límite:

def test_edad_17_no_puede_registrarse():
    assert puede_registrarse(17) is False


def test_edad_18_puede_registrarse():
    assert puede_registrarse(18) is True

El valor límite suele revelar comparaciones incorrectas como > en lugar de >=.

40.9 Error 8: copiar la implementación en la prueba

Una prueba pierde valor si calcula el resultado esperado con la misma lógica que el código productivo.

def test_descuento():
    precio = 1000
    porcentaje = 10

    assert aplicar_descuento(precio, porcentaje) == precio - (precio * porcentaje / 100)

Es mejor escribir el resultado esperado de manera explícita:

def test_descuento_del_10_por_ciento_sobre_1000_devuelve_900():
    assert aplicar_descuento(1000, 10) == 900

La prueba debe representar un ejemplo esperado, no repetir el algoritmo.

40.10 Error 9: probar detalles internos

Una prueba acoplada a detalles internos se rompe cuando refactorizamos el código, aunque el comportamiento siga siendo correcto.

def test_carrito_agrega_producto():
    carrito = Carrito()

    carrito.agregar("mouse", 1000)

    assert carrito._productos[0]["nombre"] == "mouse"

Si la clase ofrece métodos públicos, conviene probar desde ellos:

def test_agregar_producto_incrementa_cantidad():
    carrito = Carrito()

    carrito.agregar("mouse", 1000)

    assert carrito.cantidad() == 1

Las pruebas deben proteger comportamiento observable, no estructuras internas accidentales.

40.11 Error 10: depender de recursos externos

Una prueba unitaria no debería depender innecesariamente de una base de datos real, una API externa, el sistema de archivos o la red. Eso la vuelve más lenta y menos determinística.

def test_cliente_vip():
    cliente = base_de_datos.obtener_cliente(10)

    assert cliente.es_vip() is True

Para probar la regla VIP, podemos usar datos en memoria:

def test_cliente_con_1000_puntos_es_vip():
    cliente = Cliente(puntos=1000)

    assert cliente.es_vip() is True

Las integraciones reales se prueban en otros niveles. La prueba unitaria debe aislar la decisión cuando sea posible.

40.12 Error 11: pruebas lentas

Una suite unitaria lenta se ejecuta con menos frecuencia. Si el equipo evita correr las pruebas porque tardan demasiado, pierden valor como retroalimentación rápida.

Algunas causas comunes de lentitud son:

  • Acceso innecesario a base de datos.
  • Llamadas reales a servicios externos.
  • Esperas con sleep.
  • Preparación demasiado pesada para cada caso.
  • Pruebas de integración mezcladas dentro de la suite unitaria.

Una prueba unitaria debería ser pequeña y rápida. Si tarda mucho, conviene revisar si realmente es unitaria.

40.13 Error 12: pruebas no determinísticas

Una prueba no determinística a veces pasa y a veces falla sin cambios en el código. Esto erosiona la confianza en la suite.

Fuentes frecuentes de no determinismo:

  • Fechas tomadas directamente del sistema.
  • Números aleatorios sin control.
  • Dependencia del orden de elementos no garantizado.
  • Servicios externos con respuestas variables.
  • Estado compartido entre pruebas.

La solución suele ser controlar la entrada: pasar la fecha como parámetro, fijar semillas, ordenar resultados o reemplazar dependencias externas.

40.14 Error 13: usar sleeps en pruebas unitarias

Usar sleep para esperar que algo ocurra suele producir pruebas lentas y frágiles. En una prueba unitaria, normalmente deberíamos poder ejecutar la unidad de forma directa y verificar el resultado sin esperar tiempo real.

def test_proceso():
    iniciar_proceso()
    time.sleep(2)

    assert proceso_terminado() is True

Si el comportamiento depende del tiempo, conviene aislarlo o inyectar un reloj controlado:

def test_cupon_vencido_no_es_valido():
    reloj = RelojFalso(fecha_actual="2026-05-10")
    cupon = Cupon(fecha_vencimiento="2026-05-09", reloj=reloj)

    assert cupon.es_valido() is False

Controlar el tiempo hace que la prueba sea rápida y repetible.

40.15 Error 14: abusar de mocks

Los mocks son útiles, pero un exceso de mocks puede hacer que la prueba verifique detalles de implementación en lugar de comportamiento real.

def test_servicio():
    repositorio = Mock()
    notificador = Mock()
    auditor = Mock()
    servicio = Servicio(repositorio, notificador, auditor)

    servicio.procesar("A-1")

    repositorio.guardar.assert_called_once()
    notificador.enviar.assert_called_once()
    auditor.registrar.assert_called_once()

Esta prueba puede ser válida si esas interacciones son el contrato. Pero si solo reflejan cómo está implementado el servicio hoy, será frágil ante refactorizaciones.

Cuando sea posible, conviene verificar resultados observables o usar fakes simples que representen el comportamiento necesario.

40.16 Error 15: fixtures demasiado grandes

Una fixture demasiado general puede ocultar el estado real de la prueba. El lector ve un nombre, pero no sabe qué datos, objetos o dependencias se prepararon.

@pytest.fixture
def contexto_completo():
    cliente = Cliente("Ana", puntos=1000)
    carrito = Carrito()
    carrito.agregar("mouse", 1000)
    repositorio = RepositorioFalso()
    servicio = ServicioCompras(repositorio)
    return cliente, carrito, repositorio, servicio

Es preferible usar fixtures pequeñas y nombradas:

@pytest.fixture
def carrito_con_mouse():
    carrito = Carrito()
    carrito.agregar("mouse", 1000)
    return carrito

La fixture debe reducir ruido, no esconder la prueba.

40.17 Error 16: modificar la prueba para que pase sin entender el fallo

Cuando una prueba falla, puede existir la tentación de cambiar el esperado hasta que pase. Eso es peligroso si no entendemos si cambió el requisito, si hay un defecto en el código o si la prueba estaba mal escrita.

Antes de modificar una prueba fallida, conviene preguntar:

  • ¿La prueba representa una regla vigente?
  • ¿El código productivo cambió correctamente?
  • ¿El resultado esperado anterior era incorrecto?
  • ¿Hay otra prueba que proteja el mismo comportamiento?

Cambiar una prueba sin análisis puede eliminar una protección importante de la suite.

40.18 Error 17: perseguir cobertura sin criterio

La cobertura de código indica qué líneas fueron ejecutadas, pero no garantiza que las pruebas verifiquen comportamientos importantes.

def test_cobertura_sin_valor():
    crear_usuario("Ana", 20)
    calcular_descuento(1000, 10)
    validar_email("ana@example.com")

Esta prueba puede ejecutar varias líneas, pero no comprueba resultados. La cobertura debe acompañarse con aserciones y casos significativos.

Una suite con menos cobertura pero buenos casos puede ser más útil que una suite con alta cobertura y pruebas superficiales.

40.19 Error 18: no actualizar pruebas cuando cambia una regla

Las pruebas funcionan como documentación ejecutable. Si una regla de negocio cambia y las pruebas quedan con la expectativa anterior, la suite deja de representar el comportamiento correcto.

Pero actualizar pruebas no significa cambiar todo sin cuidado. Primero hay que identificar qué pruebas describen la regla antigua y qué nuevos casos deben agregarse para la regla nueva.

Cuando cambia una política importante, conviene revisar nombres, datos límite, mensajes de error y casos relacionados para que la suite vuelva a ser coherente.

40.20 Error 19: probar código trivial sin valor

No todo método necesita una prueba directa. Probar getters, setters o delegaciones sin lógica puede aportar poco si ya están cubiertos por comportamientos más importantes.

def test_get_nombre():
    usuario = Usuario("Ana")

    assert usuario.nombre == "Ana"

Esta prueba puede ser útil si la creación del usuario tiene reglas propias. Pero si solo comprueba una asignación directa del lenguaje, tal vez no sea prioritaria.

El foco debe estar en comportamientos con riesgo: reglas, cálculos, límites, errores esperados y decisiones del dominio.

40.21 Tabla de errores y correcciones

Esta tabla resume varios errores frecuentes y una forma práctica de corregirlos.

Error Consecuencia Corrección
Sin aserciones. La prueba puede pasar sin verificar nada. Agregar resultado esperado explícito.
Nombre genérico. Falla difícil de interpretar. Nombrar el comportamiento esperado.
Estado compartido. Dependencia entre pruebas. Crear datos nuevos por prueba.
Datos confusos. Intención poco clara. Usar valores simples y representativos.
Detalles internos. Pruebas frágiles ante refactorización. Probar comportamiento público observable.

40.22 Señales de una suite problemática

Además de mirar pruebas individuales, conviene observar señales generales de la suite:

  • El equipo evita ejecutar las pruebas porque son lentas.
  • Hay fallas intermitentes que se ignoran.
  • Muchas pruebas se rompen ante refactorizaciones internas pequeñas.
  • Los nombres no explican qué comportamiento falló.
  • Hay mucha preparación repetida y difícil de leer.
  • Las pruebas dependen de datos externos poco controlados.
  • Las fallas se arreglan cambiando esperados sin revisar la regla.

Estas señales indican que la suite necesita mantenimiento, no necesariamente que las pruebas unitarias sean una mala herramienta.

40.23 Qué debes recordar de este tema

  • Una prueba sin aserción clara aporta poco valor.
  • Los nombres deben describir comportamientos esperados.
  • Cada prueba debe ser independiente y determinística.
  • Los datos simples ayudan a entender la regla probada.
  • Los casos límite suelen detectar errores importantes.
  • Probar detalles internos vuelve la suite frágil.
  • La cobertura de código no reemplaza pruebas con intención clara.

40.24 Conclusión

Los errores al escribir pruebas unitarias no siempre son evidentes. Una prueba puede pasar durante meses y aun así no proteger un comportamiento relevante, ser demasiado frágil o dificultar el mantenimiento.

La calidad de una prueba depende de su intención, sus datos, sus aserciones y su independencia. Una suite útil debe dar retroalimentación rápida y confiable, no convertirse en una carga que el equipo aprende a ignorar.

En el próximo tema veremos buenas prácticas para mantener una suite rápida y clara, tomando estos errores como punto de partida para construir hábitos sostenibles.