Una prueba unitaria debe poder ejecutarse sola, junto con otras pruebas y en cualquier orden. A esto lo llamamos independencia entre pruebas.
Cuando las pruebas dependen unas de otras, la suite se vuelve frágil. Puede pasar completa en un orden, fallar en otro o producir resultados distintos según qué prueba se ejecutó antes.
En este tema veremos por qué la independencia es importante, qué problemas aparecen cuando se rompe y cómo escribir pruebas que no dependan de estado compartido.
Una prueba independiente cumple estas condiciones:
Veamos un ejemplo problemático:
usuarios = []
def test_agregar_usuario():
usuarios.append("Ana")
assert len(usuarios) == 1
def test_usuario_ana_existe():
assert "Ana" in usuarios
La segunda prueba depende de que la primera se haya ejecutado antes. Si ejecutamos solo test_usuario_ana_existe, fallará. Si el framework cambia el orden, también puede fallar.
Cada prueba debe preparar su propio contexto.
def test_agregar_usuario_incrementa_cantidad():
usuarios = []
usuarios.append("Ana")
assert len(usuarios) == 1
def test_usuario_ana_existe_despues_de_agregarlo():
usuarios = []
usuarios.append("Ana")
assert "Ana" in usuarios
Ahora cada prueba puede ejecutarse sola. No depende del estado dejado por otra prueba.
El estado compartido mutable es una de las causas más comunes de pruebas dependientes. Ocurre cuando varias pruebas usan y modifican el mismo objeto, lista, diccionario, archivo o variable global.
Problemas típicos:
La solución suele ser crear datos nuevos para cada prueba o limpiar el estado de manera controlada.
Crear objetos nuevos dentro de cada prueba es una forma simple de mantener independencia.
class Carrito:
def __init__(self):
self.items = []
def agregar_item(self, item):
self.items.append(item)
def test_carrito_nuevo_esta_vacio():
carrito = Carrito()
assert carrito.items == []
def test_agregar_item_al_carrito():
carrito = Carrito()
carrito.agregar_item("teclado")
assert carrito.items == ["teclado"]
Cada prueba crea su propio carrito. Ninguna depende del carrito de otra.
Una fixture puede ayudar a crear datos nuevos para cada prueba, siempre que no comparta estado mutable de forma peligrosa.
def crear_carrito_vacio():
return Carrito()
def test_carrito_nuevo_esta_vacio():
carrito = crear_carrito_vacio()
assert carrito.items == []
def test_agregar_item_al_carrito():
carrito = crear_carrito_vacio()
carrito.agregar_item("teclado")
assert carrito.items == ["teclado"]
El helper devuelve un objeto nuevo cada vez. Eso mantiene la independencia.
Un helper o fixture puede ser riesgoso si devuelve siempre el mismo objeto mutable.
carrito_compartido = Carrito()
def obtener_carrito():
return carrito_compartido
Si varias pruebas usan obtener_carrito() y modifican el carrito, una prueba puede afectar a otra. Es mejor crear una instancia nueva por prueba.
Las pruebas que escriben archivos pueden dejar residuos para otras pruebas. Si una prueba crea un archivo y otra lo encuentra, el resultado puede depender del orden o del estado anterior del entorno.
Buenas prácticas:
En pruebas unitarias, conviene evitar archivos reales salvo que la unidad sea justamente una función de manejo de archivos.
Una prueba unitaria normalmente no debería depender de una base de datos real. Si lo hace, puede quedar acoplada al estado que otras pruebas dejan en esa base.
Problemas frecuentes:
Estos casos suelen corresponder más a pruebas de integración. Para unitarias, conviene aislar la lógica y usar datos controlados en memoria.
Modificar configuración global dentro de una prueba puede afectar otras pruebas si no se restaura.
configuracion = {"moneda": "ARS"}
def test_cambiar_moneda_a_dolares():
configuracion["moneda"] = "USD"
assert configuracion["moneda"] == "USD"
Si otra prueba espera moneda ARS, puede fallar dependiendo del orden. En general, conviene evitar mutar configuración global o restaurarla después de la prueba.
Si una prueba debe modificar algo compartido, debe restaurarlo. Aun así, en pruebas unitarias suele ser mejor evitar este patrón cuando sea posible.
def test_cambiar_moneda_temporalmente():
moneda_original = configuracion["moneda"]
try:
configuracion["moneda"] = "USD"
assert configuracion["moneda"] == "USD"
finally:
configuracion["moneda"] = moneda_original
El bloque finally intenta dejar el estado como estaba, incluso si la aserción falla.
Las suites modernas pueden ejecutar pruebas en paralelo para ahorrar tiempo. Si las pruebas comparten estado mutable, la ejecución paralela puede revelar problemas que antes no se veían.
Una prueba independiente es más fácil de ejecutar en paralelo porque no compite por los mismos datos ni depende de un orden específico.
La independencia mejora tanto la confiabilidad como la velocidad potencial de la suite.
Señales de dependencia:
Si aparece alguna de estas señales, debemos buscar estado compartido o dependencia de orden.
Algunas herramientas permiten ejecutar pruebas en orden aleatorio. Esto puede ayudar a descubrir dependencias ocultas.
Si una suite solo pasa en un orden específico, hay un problema de independencia. El orden no debería ser parte del contrato de una prueba unitaria.
No es obligatorio usar orden aleatorio siempre, pero puede ser una técnica útil para detectar acoplamientos.
Una prueba no debe actuar como paso previo de otra. Las pruebas no son un guion secuencial; son verificaciones independientes.
Ejemplo incorrecto:
Ese patrón se parece más a un flujo de integración o end-to-end. En pruebas unitarias, cada caso debe preparar su propio estado.
Preparar datos en cada prueba no significa copiar bloques enormes. Podemos usar helpers o fixtures siempre que devuelvan datos nuevos y mantengan visible lo importante.
def crear_cuenta_con_saldo(saldo):
return Cuenta(saldo)
def test_depositar_incrementa_saldo():
cuenta = crear_cuenta_con_saldo(100)
cuenta.depositar(50)
assert cuenta.saldo == 150
La prueba sigue siendo independiente porque recibe una cuenta nueva.
| Problema | Riesgo | Solución |
|---|---|---|
| Lista global modificada. | Resultados dependientes del orden. | Crear lista nueva por prueba. |
| Objeto compartido mutable. | Una prueba afecta a otra. | Usar factory o fixture que cree instancias nuevas. |
| Archivos persistentes. | Residuos entre ejecuciones. | Usar temporales y limpieza. |
| Configuración global modificada. | Fallas intermitentes. | Evitar mutación o restaurar estado. |
| Una prueba prepara otra. | No se puede ejecutar una prueba aislada. | Cada prueba prepara su propio contexto. |
Para verificar independencia, revisa:
La independencia entre pruebas es una condición básica para confiar en una suite unitaria. Si una prueba depende del orden, del estado dejado por otra o de datos compartidos, sus resultados pueden volverse impredecibles.
Una buena prueba prepara su propio contexto, ejecuta su acción y verifica su resultado sin requerir que otra prueba haya corrido antes.
En el próximo tema estudiaremos pruebas determinísticas y repetibles, un principio muy relacionado con la independencia.