23. Independencia entre pruebas

23.1 Introducción

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.

23.2 Qué significa que una prueba sea independiente

Una prueba independiente cumple estas condiciones:

  • Prepara sus propios datos.
  • No necesita que otra prueba se ejecute antes.
  • No deja estado que afecte a otras pruebas.
  • Puede ejecutarse en cualquier orden.
  • Produce el mismo resultado si el código no cambió.
Una prueba debe ser una unidad de verificación completa: prepara, ejecuta y verifica sin depender de otra prueba.

23.3 Ejemplo de dependencia de orden

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.

23.4 Versión independiente

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.

23.5 Estado compartido mutable

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:

  • Una prueba agrega datos que otra encuentra inesperadamente.
  • Una prueba borra datos que otra necesitaba.
  • Una prueba modifica configuración global.
  • El resultado depende del orden de ejecución.

La solución suele ser crear datos nuevos para cada prueba o limpiar el estado de manera controlada.

23.6 Objetos nuevos por prueba

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.

23.7 Fixtures y aislamiento

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.

23.8 Fixture riesgosa

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.

23.9 Dependencia de archivos

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:

  • Usar archivos temporales.
  • Limpiar los archivos creados.
  • No depender de archivos generados por otras pruebas.
  • Preferir datos en memoria si alcanza para una prueba unitaria.

En pruebas unitarias, conviene evitar archivos reales salvo que la unidad sea justamente una función de manejo de archivos.

23.10 Dependencia de base de datos

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:

  • IDs que cambian según pruebas anteriores.
  • Registros duplicados.
  • Datos que no se limpian.
  • Orden de ejecución que afecta resultados.

Estos casos suelen corresponder más a pruebas de integración. Para unitarias, conviene aislar la lógica y usar datos controlados en memoria.

23.11 Dependencia de configuración global

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.

23.12 Restaurar estado cuando sea necesario

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.

23.13 Pruebas independientes y ejecución paralela

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.

23.14 Cómo detectar dependencia entre pruebas

Señales de dependencia:

  • Una prueba pasa al ejecutar toda la suite, pero falla sola.
  • Una prueba pasa sola, pero falla al ejecutar con otras.
  • El resultado cambia según el orden de ejecución.
  • La suite falla de forma intermitente.
  • Hay variables globales modificadas por pruebas.
  • Hay archivos o datos persistentes compartidos.

Si aparece alguna de estas señales, debemos buscar estado compartido o dependencia de orden.

23.15 Probar en orden aleatorio

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.

23.16 No usar una prueba como preparación de otra

Una prueba no debe actuar como paso previo de otra. Las pruebas no son un guion secuencial; son verificaciones independientes.

Ejemplo incorrecto:

  • Primera prueba: crea un usuario.
  • Segunda prueba: actualiza ese usuario.
  • Tercera prueba: elimina ese usuario.

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.

23.17 Independencia no significa duplicar sin criterio

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.

23.18 Tabla de problemas y soluciones

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.

23.19 Lista de comprobación

Para verificar independencia, revisa:

  • ¿La prueba puede ejecutarse sola?
  • ¿Prepara todos los datos que necesita?
  • ¿Evita depender de datos creados por otra prueba?
  • ¿No modifica estado global sin restaurarlo?
  • ¿Usa objetos nuevos o datos aislados?
  • ¿El resultado es igual si cambia el orden de ejecución?
  • ¿No deja archivos, registros o datos persistentes que afecten a otros casos?

23.20 Qué debes recordar de este tema

  • Una prueba independiente puede ejecutarse sola y en cualquier orden.
  • Las pruebas no deben depender de otras pruebas.
  • El estado compartido mutable es una causa común de fallas intermitentes.
  • Cada prueba debe preparar su propio contexto.
  • Los helpers son útiles si crean datos nuevos y no ocultan lo importante.
  • Modificar configuración global o archivos requiere cuidado especial.
  • La independencia mejora la confiabilidad de la suite.

23.21 Conclusión

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.