24. Pruebas determinísticas y repetibles

24.1 Introducción

Una prueba unitaria debe producir el mismo resultado cuando se ejecuta varias veces bajo las mismas condiciones. Si el código no cambió, la prueba no debería pasar algunas veces y fallar otras.

A esa propiedad la llamamos determinismo. Una prueba determinística es predecible: con los mismos datos y el mismo código, obtiene el mismo resultado.

En este tema veremos por qué algunas pruebas se vuelven variables y cómo hacerlas repetibles.

24.2 Qué significa determinístico

Una prueba determinística tiene estas características:

  • El resultado no depende del momento del día.
  • No depende del orden en que se ejecutaron otras pruebas.
  • No depende de datos externos cambiantes.
  • No depende de respuestas de red o servicios externos.
  • No depende de azar sin controlar.
Si una prueba puede pasar o fallar sin que el código cambie, la prueba no es confiable.

24.3 Pruebas intermitentes

Una prueba intermitente, también llamada flaky, es una prueba que a veces pasa y a veces falla sin una causa visible inmediata.

Estas pruebas son peligrosas porque dañan la confianza en la suite. Si el equipo se acostumbra a fallas intermitentes, puede empezar a ignorar fallas reales.

Cuando una prueba es intermitente, no conviene aceptarla como "normal". Hay que buscar la fuente de variabilidad.

24.4 Causa frecuente: fecha actual

Una prueba puede fallar si depende de la fecha actual del sistema.

from datetime import date


def cupon_vigente(fecha_vencimiento):
    return date.today() <= fecha_vencimiento


def test_cupon_vigente():
    assert cupon_vigente(date(2026, 12, 31)) == True

Esta prueba pasará antes o durante el 31 de diciembre de 2026, pero fallará después. El resultado depende del día en que se ejecute.

24.5 Solución: pasar la fecha como dato

Una forma de hacer la prueba determinística es pasar la fecha actual como parámetro.

from datetime import date


def cupon_vigente(fecha_actual, fecha_vencimiento):
    return fecha_actual <= fecha_vencimiento


def test_cupon_vigente_en_fecha_anterior_al_vencimiento():
    fecha_actual = date(2026, 6, 1)
    fecha_vencimiento = date(2026, 12, 31)

    assert cupon_vigente(fecha_actual, fecha_vencimiento) == True


def test_cupon_no_vigente_despues_del_vencimiento():
    fecha_actual = date(2027, 1, 1)
    fecha_vencimiento = date(2026, 12, 31)

    assert cupon_vigente(fecha_actual, fecha_vencimiento) == False

Ahora la prueba no depende del reloj real. La fecha es un dato controlado.

24.6 Causa frecuente: aleatoriedad

Si una unidad usa valores aleatorios, la prueba puede recibir resultados distintos en cada ejecución.

import random


def generar_codigo():
    return random.randint(1000, 9999)


def test_generar_codigo_tiene_cuatro_digitos():
    codigo = generar_codigo()

    assert codigo >= 1000 and codigo <= 9999

Esta prueba puede parecer estable, pero si la regla cambia o el rango se usa de forma más compleja, la aleatoriedad puede dificultar el diagnóstico. En general, conviene controlar el azar en pruebas unitarias.

24.7 Solución: controlar el valor aleatorio

Una opción es separar la generación aleatoria de la lógica que queremos probar.

def formato_codigo(numero):
    return f"COD-{numero}"


def test_formato_codigo():
    assert formato_codigo(1234) == "COD-1234"

La prueba verifica la lógica de formato con un dato fijo. La generación aleatoria puede probarse de otra manera o quedar fuera de la unidad principal.

24.8 Causa frecuente: datos compartidos

Si una prueba usa datos modificados por otra, su resultado puede cambiar según el orden de ejecución.

usuarios = []


def test_agregar_usuario():
    usuarios.append("Ana")

    assert len(usuarios) == 1

Si otra prueba también modifica usuarios, el tamaño esperado puede no ser 1. La prueba deja de ser repetible.

24.9 Solución: datos nuevos por prueba

def test_agregar_usuario():
    usuarios = []

    usuarios.append("Ana")

    assert len(usuarios) == 1

La lista se crea dentro de la prueba. Cada ejecución empieza desde el mismo estado inicial.

24.10 Causa frecuente: dependencias externas

Una prueba unitaria no debería depender de servicios externos como APIs, red, base de datos real o sistema de archivos externo. Esas dependencias pueden cambiar, fallar o tardar.

Problemas típicos:

  • La red no está disponible.
  • La API externa responde distinto.
  • La base de datos tiene datos diferentes.
  • Un archivo esperado no existe.
  • El servicio tarda más de lo previsto.

Estos problemas hacen que la prueba falle por motivos ajenos a la unidad.

24.11 Solución: aislar la lógica

Si queremos probar una regla de negocio, conviene separarla de la dependencia externa.

def calcular_total_con_impuesto(precio, impuesto):
    return precio + (precio * impuesto / 100)


def test_calcular_total_con_impuesto():
    assert calcular_total_con_impuesto(1000, 21) == 1210

La prueba no necesita consultar una API ni una base de datos para verificar la fórmula. La integración con recursos externos se prueba en otro nivel.

24.12 Causa frecuente: orden de elementos

Algunas estructuras no garantizan orden. Si una prueba exige un orden que la unidad no promete, puede volverse frágil.

def obtener_roles():
    return {"admin", "editor"}


def test_obtener_roles():
    roles = list(obtener_roles())

    assert roles == ["admin", "editor"]

Un conjunto no garantiza orden. La prueba podría fallar si los elementos aparecen invertidos.

24.13 Solución: verificar sin depender del orden

def test_obtener_roles():
    roles = obtener_roles()

    assert roles == {"admin", "editor"}

Ahora la prueba verifica el contenido sin imponer un orden innecesario.

24.14 Causa frecuente: precisión numérica

Los cálculos con números decimales pueden producir pequeñas diferencias de precisión.

def test_promedio_decimal():
    resultado = promedio([0.1, 0.2])

    assert resultado == 0.15

Dependiendo del lenguaje y del tipo numérico, esta comparación exacta puede ser problemática. Si trabajamos con punto flotante, conviene usar tolerancia.

24.15 Solución: tolerancia para decimales

def test_promedio_decimal():
    resultado = promedio([0.1, 0.2])

    assert abs(resultado - 0.15) < 0.0001

La prueba acepta una diferencia muy pequeña. Esto hace que la verificación sea adecuada para el tipo de dato usado.

24.16 Repetibilidad en cualquier entorno

Una prueba repetible debería comportarse igual en la computadora de una persona, en la de otra y en un servidor de integración continua.

Factores que pueden variar entre entornos:

  • Zona horaria.
  • Idioma o configuración regional.
  • Rutas de archivos.
  • Variables de entorno.
  • Versiones de dependencias.

Las pruebas unitarias deben minimizar estas dependencias o controlarlas explícitamente.

24.17 Cómo detectar pruebas no determinísticas

Señales de alerta:

  • La prueba falla una vez y luego pasa sin cambios.
  • Falla en integración continua, pero no localmente.
  • Falla solo en ciertos horarios o fechas.
  • Falla cuando se ejecuta con toda la suite, pero no sola.
  • Falla cuando se ejecuta en otro orden.

Estas señales indican que el resultado depende de algo externo al comportamiento probado.

24.18 Tabla de causas y soluciones

Causa Riesgo Solución
Fecha actual La prueba cambia con el tiempo. Pasar la fecha como dato controlado.
Aleatoriedad Resultados distintos por ejecución. Separar lógica o fijar entrada.
Estado compartido Dependencia de orden. Crear datos nuevos por prueba.
Dependencias externas Fallas ajenas a la unidad. Aislar la lógica o usar dobles de prueba.
Orden no garantizado Fallas por comparación rígida. Verificar contenido sin exigir orden innecesario.
Decimales Diferencias pequeñas de precisión. Usar tolerancia o tipos adecuados.

24.19 Lista de comprobación

Para revisar si una prueba es repetible, pregunta:

  • ¿Depende de la fecha u hora actual?
  • ¿Usa valores aleatorios sin control?
  • ¿Comparte datos modificables con otras pruebas?
  • ¿Depende de red, base de datos o archivos externos?
  • ¿Exige un orden que la unidad no garantiza?
  • ¿Puede ejecutarse sola y obtener el mismo resultado?
  • ¿Funciona igual en otro entorno?

24.20 Qué debes recordar de este tema

  • Una prueba determinística produce el mismo resultado con el mismo código y datos.
  • Las pruebas intermitentes reducen la confianza en la suite.
  • Fecha actual, azar, estado compartido y dependencias externas son causas frecuentes de variabilidad.
  • Conviene pasar datos controlados en lugar de depender del entorno.
  • Las pruebas unitarias deben minimizar dependencias externas.
  • El orden de elementos solo debe verificarse si forma parte del comportamiento esperado.
  • Una suite confiable necesita pruebas repetibles.

24.21 Conclusión

Las pruebas determinísticas y repetibles son esenciales para confiar en una suite unitaria. Si una prueba falla sin cambios en el código, deja de ser una señal clara.

El objetivo es controlar las condiciones de la prueba: datos, fechas, estado, orden y dependencias. Cuanto menos dependa del entorno, más útil será la prueba.

En el próximo tema veremos cómo evitar dependencias externas en pruebas unitarias.