20. Pruebas determinísticas: controlar fechas, aleatoriedad y dependencias externas

20.1 Objetivo del tema

Una prueba determinística produce el mismo resultado cada vez que se ejecuta, siempre que el código no haya cambiado. Esto es fundamental para confiar en una suite automatizada.

En este tema veremos cómo controlar fechas, aleatoriedad y dependencias externas para evitar pruebas que pasan o fallan por factores ajenos al comportamiento que queremos verificar.

Objetivo práctico: eliminar fuentes de variabilidad para que las pruebas sean repetibles y confiables.

20.2 Qué vuelve no determinística a una prueba

Una prueba puede volverse inestable si depende de elementos que cambian entre ejecuciones:

  • Fecha u hora actual.
  • Números aleatorios sin semilla.
  • Servicios externos.
  • Archivos compartidos entre pruebas.
  • Orden no garantizado de datos.
  • Variables de entorno reales del equipo.

20.3 Ejemplo de prueba frágil por fecha actual

Crea app/promociones.py:

from datetime import date


def es_promocion_de_fin_de_anio():
    hoy = date.today()
    return hoy.month == 12 and hoy.day == 31

Esta función depende de la fecha real. Una prueba directa podría pasar solo el 31 de diciembre.

20.4 Mejorar el diseño pasando la fecha

Una forma simple de hacer la función testeable es recibir la fecha como parámetro:

def es_promocion_de_fin_de_anio(fecha):
    return fecha.month == 12 and fecha.day == 31

Ahora la prueba controla el dato:

from datetime import date

from app.promociones import es_promocion_de_fin_de_anio


def test_es_promocion_de_fin_de_anio_con_31_de_diciembre_devuelve_true():
    assert es_promocion_de_fin_de_anio(date(2026, 12, 31)) is True


def test_es_promocion_de_fin_de_anio_con_30_de_diciembre_devuelve_false():
    assert es_promocion_de_fin_de_anio(date(2026, 12, 30)) is False

20.5 Ejecutar las pruebas de fecha

Ejecuta:

python -m pytest tests/test_promociones.py

Estas pruebas no dependen del día real en que se ejecutan.

20.6 Controlar fecha con una función proveedora

Otra opción es separar la obtención de la fecha actual:

from datetime import date


def obtener_fecha_actual():
    return date.today()


def es_promocion_hoy():
    hoy = obtener_fecha_actual()
    return hoy.month == 12 and hoy.day == 31

Esto permite reemplazar obtener_fecha_actual durante la prueba.

20.7 Reemplazar una función con monkeypatch

Podemos usar monkeypatch para controlar la fecha:

from datetime import date

import app.promociones as promociones


def test_es_promocion_hoy_con_fecha_controlada(monkeypatch):
    monkeypatch.setattr(
        promociones,
        "obtener_fecha_actual",
        lambda: date(2026, 12, 31),
    )

    assert promociones.es_promocion_hoy() is True

La prueba reemplaza temporalmente la función que obtiene la fecha real.

20.8 Aleatoriedad no controlada

Una función que usa aleatoriedad puede producir resultados distintos:

import random


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

Probar un valor exacto sería incorrecto si no controlamos la fuente aleatoria.

20.9 Usar semilla para aleatoriedad

Podemos recibir una semilla o un generador:

import random


def generar_codigo(seed=None):
    generador = random.Random(seed)
    return generador.randint(1000, 9999)

Prueba:

from app.codigos import generar_codigo


def test_generar_codigo_con_misma_semilla_devuelve_mismo_valor():
    assert generar_codigo(seed=1234) == generar_codigo(seed=1234)

20.10 Verificar propiedades en lugar de valores exactos

A veces alcanza con verificar propiedades del resultado:

def test_generar_codigo_devuelve_numero_de_cuatro_digitos():
    codigo = generar_codigo(seed=1234)

    assert 1000 <= codigo <= 9999

La prueba no depende de un número específico, pero sigue verificando una regla útil.

20.11 Reemplazar aleatoriedad con monkeypatch

También podemos reemplazar una función aleatoria:

import random

from app.codigos import generar_codigo


def test_generar_codigo_con_randint_controlado(monkeypatch):
    monkeypatch.setattr(random, "randint", lambda minimo, maximo: 1234)

    assert generar_codigo() == 1234

Esta técnica debe usarse con cuidado y solo cuando el diseño no permite inyectar la dependencia más fácilmente.

20.12 Dependencias externas

Una dependencia externa puede ser una API, una base de datos, un archivo remoto o cualquier recurso fuera del control directo de la prueba.

En este curso no entraremos todavía en testing de APIs REST, pero sí podemos practicar la idea de reemplazar dependencias externas por funciones controladas.

20.13 Crear un módulo con dependencia externa simulada

Crea app/cotizaciones.py:

def obtener_cotizacion_dolar():
    raise RuntimeError("Servicio externo no disponible en pruebas")


def convertir_pesos_a_dolares(monto_pesos):
    cotizacion = obtener_cotizacion_dolar()
    return monto_pesos / cotizacion

La función obtener_cotizacion_dolar representa una dependencia externa.

20.14 Reemplazar dependencia externa

En la prueba, reemplazamos la dependencia por un valor controlado:

import app.cotizaciones as cotizaciones


def test_convertir_pesos_a_dolares_con_cotizacion_controlada(monkeypatch):
    monkeypatch.setattr(cotizaciones, "obtener_cotizacion_dolar", lambda: 1000)

    assert cotizaciones.convertir_pesos_a_dolares(5000) == 5

La prueba no llama a ningún servicio real y siempre usa la misma cotización.

20.15 Inyección de dependencias como alternativa

Otra opción es pasar la dependencia como parámetro:

def convertir_pesos_a_dolares(monto_pesos, obtener_cotizacion):
    cotizacion = obtener_cotizacion()
    return monto_pesos / cotizacion

Prueba:

def test_convertir_pesos_a_dolares_con_funcion_inyectada():
    def cotizacion_fija():
        return 1000

    assert convertir_pesos_a_dolares(5000, cotizacion_fija) == 5

Este diseño suele ser más fácil de probar porque no necesita modificar el módulo durante la prueba.

20.16 Evitar dependencias entre pruebas

Una prueba no debe depender de que otra se haya ejecutado antes. Cada prueba debe preparar sus propios datos y dependencias.

Incorrecto:

resultado_global = None


def test_prepara_resultado():
    global resultado_global
    resultado_global = 10


def test_usa_resultado():
    assert resultado_global == 10

La segunda prueba depende de la primera. Eso rompe el aislamiento.

20.17 Controlar orden no garantizado

Si una función devuelve elementos sin orden garantizado, la prueba no debe asumir un orden arbitrario.

def test_lista_de_roles_sin_importar_orden():
    roles = ["admin", "editor", "lector"]

    assert set(roles) == {"admin", "editor", "lector"}

Si el orden sí es parte del comportamiento, entonces corresponde probarlo como una regla explícita.

20.18 Marcar pruebas potencialmente frágiles

Si una prueba depende de algo lento o externo, conviene revisarla y posiblemente marcarla:

import pytest


@pytest.mark.lento
def test_proceso_costoso_controlado():
    assert True

Pero marcar una prueba como lenta no soluciona la fragilidad. Primero debemos intentar controlar sus dependencias.

20.19 Problemas frecuentes

  • La prueba falla solo algunos días: probablemente depende de la fecha actual.
  • La prueba falla al azar: revisa si usa números aleatorios sin semilla.
  • La prueba falla sin conexión: puede depender de un servicio externo.
  • La prueba falla cuando cambia el orden: revisa si el orden realmente importa.
  • Una prueba depende de otra: mueve la preparación a fixtures o factories.

20.20 Ejercicio práctico

Crea app/tickets.py con una función generar_ticket. Debe recibir una función que devuelva la fecha y una función que devuelva un número. El ticket debe tener formato YYYYMMDD-NUMERO.

Luego crea pruebas controlando ambas dependencias para verificar que el resultado sea repetible.

20.21 Solución propuesta

Archivo app/tickets.py:

def generar_ticket(obtener_fecha, obtener_numero):
    fecha = obtener_fecha()
    numero = obtener_numero()
    return f"{fecha:%Y%m%d}-{numero}"

Prueba:

from datetime import date

from app.tickets import generar_ticket


def test_generar_ticket_con_dependencias_controladas_devuelve_ticket_esperado():
    def fecha_fija():
        return date(2026, 5, 10)

    def numero_fijo():
        return 1234

    assert generar_ticket(fecha_fija, numero_fijo) == "20260510-1234"

Ejecuta:

python -m pytest tests/test_tickets.py

20.22 Lista de verificación

Antes de continuar con el próximo tema, verifica lo siguiente:

  • No dependes de la fecha actual sin control.
  • No usas aleatoriedad sin semilla o sin reemplazo controlado.
  • No llamas servicios externos reales en pruebas básicas.
  • Cada prueba prepara sus propios datos.
  • Usas monkeypatch o inyección de dependencias cuando corresponde.
  • No asumes orden si el orden no forma parte del comportamiento.
  • La suite se ejecuta correctamente con python -m pytest.

20.23 Conclusión

En este tema vimos cómo escribir pruebas determinísticas controlando fechas, aleatoriedad y dependencias externas. Una suite confiable debe fallar por problemas reales del código, no por factores cambiantes del entorno.

En el próximo tema veremos reintentos, esperas y pruebas frágiles: cuándo usarlos y cuándo evitarlos.