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.
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.
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.
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.
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.
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.
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.
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 >=.
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.
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.
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.
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:
sleep.Una prueba unitaria debería ser pequeña y rápida. Si tarda mucho, conviene revisar si realmente es unitaria.
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:
La solución suele ser controlar la entrada: pasar la fecha como parámetro, fijar semillas, ordenar resultados o reemplazar dependencias externas.
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.
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.
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.
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:
Cambiar una prueba sin análisis puede eliminar una protección importante de la suite.
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.
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.
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.
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. |
Además de mirar pruebas individuales, conviene observar señales generales de la suite:
Estas señales indican que la suite necesita mantenimiento, no necesariamente que las pruebas unitarias sean una mala herramienta.
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.