22. Controlar fechas, horas, aleatoriedad e identificadores generados

22.1 Objetivo del tema

Las pruebas deben ser determinísticas: el mismo código, con los mismos datos, debería producir el mismo resultado. Fechas, horas, números aleatorios e identificadores generados pueden romper esa estabilidad.

En este tema veremos cómo controlar esas fuentes de variación usando inyección de dependencias, patch, monkeypatch y mocks.

Objetivo práctico: hacer predecibles las pruebas que dependen del tiempo, la aleatoriedad o identificadores únicos.

22.2 Problema con la fecha actual

Supongamos esta función:

from datetime import date


def crear_numero_factura(cliente_id):
    hoy = date.today()
    return f"FAC-{hoy.year}-{cliente_id}"

El resultado cambia según el año actual. La prueba debería controlar la fecha para no depender del día en que se ejecuta.

22.3 Controlar date.today con patch

Si la función está en facturas.py, parcheamos el nombre date usado por ese módulo:

from datetime import date
from unittest.mock import patch

from facturas import crear_numero_factura


def test_crear_numero_factura():
    with patch("facturas.date") as date_mock:
        date_mock.today.return_value = date(2026, 5, 15)

        numero = crear_numero_factura("CLI-10")

    assert numero == "FAC-2026-CLI-10"

La prueba ya no depende de la fecha real.

22.4 Inyectar la fecha

Otra opción, muchas veces más simple, es recibir la fecha como parámetro:

def crear_numero_factura(cliente_id, hoy):
    return f"FAC-{hoy.year}-{cliente_id}"

La prueba:

from datetime import date


def test_crear_numero_factura_con_fecha_inyectada():
    numero = crear_numero_factura("CLI-10", date(2026, 5, 15))

    assert numero == "FAC-2026-CLI-10"

Si el diseño lo permite, inyectar la fecha suele ser más claro que parchear.

22.5 Inyectar una función reloj

Cuando muchas funciones necesitan la fecha actual, podemos inyectar una función reloj:

def crear_numero_factura(cliente_id, obtener_hoy):
    hoy = obtener_hoy()
    return f"FAC-{hoy.year}-{cliente_id}"

Prueba:

def test_crear_numero_factura_con_reloj():
    def hoy_fijo():
        return date(2026, 5, 15)

    numero = crear_numero_factura("CLI-10", hoy_fijo)

    assert numero == "FAC-2026-CLI-10"

22.6 Controlar datetime.now

Función:

from datetime import datetime


def registrar_evento(nombre):
    return {
        "nombre": nombre,
        "creado_en": datetime.now().isoformat(),
    }

Prueba con patch:

from datetime import datetime
from unittest.mock import patch


def test_registrar_evento():
    fecha_fija = datetime(2026, 5, 15, 10, 30, 0)

    with patch("eventos.datetime") as datetime_mock:
        datetime_mock.now.return_value = fecha_fija

        evento = registrar_evento("login")

    assert evento == {
        "nombre": "login",
        "creado_en": "2026-05-15T10:30:00",
    }

De nuevo, se parchea el nombre usado por el módulo bajo prueba.

22.7 Identificadores con uuid4

Función:

from uuid import uuid4


def crear_id_pedido():
    return f"PED-{uuid4()}"

Prueba:

from unittest.mock import patch


def test_crear_id_pedido():
    with patch("pedidos.uuid4") as uuid4_mock:
        uuid4_mock.return_value = "ABC123"

        pedido_id = crear_id_pedido()

    assert pedido_id == "PED-ABC123"
    uuid4_mock.assert_called_once_with()

22.8 Inyectar generador de identificadores

Una alternativa sin patch:

def crear_id_pedido(generar_uuid):
    return f"PED-{generar_uuid()}"

Prueba:

def test_crear_id_pedido_con_generador_inyectado():
    pedido_id = crear_id_pedido(lambda: "ABC123")

    assert pedido_id == "PED-ABC123"

Este enfoque reduce el acoplamiento con imports y facilita pruebas simples.

22.9 Aleatoriedad con random

Función:

import random


def elegir_descuento():
    return random.choice([5, 10, 15])

Prueba con patch:

from unittest.mock import patch


def test_elegir_descuento():
    with patch("promociones.random.choice", return_value=10) as choice_mock:
        descuento = elegir_descuento()

    assert descuento == 10
    choice_mock.assert_called_once_with([5, 10, 15])

22.10 Inyectar generador aleatorio

También podemos diseñar la función para recibir el selector:

def elegir_descuento(elegir):
    return elegir([5, 10, 15])

Prueba:

def test_elegir_descuento_con_selector_inyectado():
    def elegir_fijo(opciones):
        return 10

    assert elegir_descuento(elegir_fijo) == 10

Si necesitamos verificar las opciones, usamos un mock:

from unittest.mock import Mock


def test_elegir_descuento_verifica_opciones():
    elegir = Mock(return_value=10)

    descuento = elegir_descuento(elegir)

    assert descuento == 10
    elegir.assert_called_once_with([5, 10, 15])

22.11 random.randint

Función:

import random


def crear_pin():
    return str(random.randint(1000, 9999))

Prueba con monkeypatch:

import seguridad.pin as pin


def test_crear_pin(monkeypatch):
    monkeypatch.setattr(pin.random, "randint", lambda inicio, fin: 1234)

    assert pin.crear_pin() == "1234"

22.12 Semillas aleatorias

Otra técnica es usar una semilla con random.seed. Puede servir en algunos casos, pero suele ser menos explícita que inyectar o parchear el generador.

random.seed(10)

El problema es que la prueba queda atada a la implementación exacta del generador y al orden de llamadas. Para pruebas unitarias, preferimos controlar directamente el valor.

22.13 Controlar valores secuenciales

Si el código genera varios identificadores, podemos usar side_effect:

def crear_ids(cantidad, generar_uuid):
    return [f"ID-{generar_uuid()}" for _ in range(cantidad)]

Prueba:

from unittest.mock import Mock


def test_crear_ids():
    generar_uuid = Mock()
    generar_uuid.side_effect = ["A", "B", "C"]

    ids = crear_ids(3, generar_uuid)

    assert ids == ["ID-A", "ID-B", "ID-C"]
    assert generar_uuid.call_count == 3

22.14 Diseñar un reloj simple

Para sistemas más grandes, puede ser útil crear un objeto reloj:

class RelojSistema:
    def ahora(self):
        return datetime.now()

El código recibe el reloj:

def crear_evento(nombre, reloj):
    return {
        "nombre": nombre,
        "creado_en": reloj.ahora(),
    }

En pruebas usamos un stub:

class RelojStub:
    def ahora(self):
        return datetime(2026, 5, 15, 10, 30, 0)

22.15 Prueba con reloj stub

Ejemplo:

def test_crear_evento_con_reloj_stub():
    evento = crear_evento("login", RelojStub())

    assert evento == {
        "nombre": "login",
        "creado_en": datetime(2026, 5, 15, 10, 30, 0),
    }

Este diseño evita parches y centraliza la dependencia temporal.

22.16 Buenas prácticas

  • No dejes que la prueba dependa de la fecha real, la hora real o valores aleatorios.
  • Inyecta fechas, relojes o generadores cuando el diseño lo permita.
  • Usa patch o monkeypatch cuando trabajes con código existente.
  • Verifica llamadas solo si la interacción importa.
  • Evita depender de semillas aleatorias si puedes controlar el valor directamente.

22.17 Ejercicio práctico

Prueba esta función:

from datetime import datetime
from uuid import uuid4


def crear_evento_auditoria(usuario_id):
    return {
        "id": str(uuid4()),
        "usuario_id": usuario_id,
        "creado_en": datetime.now().isoformat(),
    }

Haz que uuid4 devuelva "EVT-1" y que datetime.now() devuelva 2026-05-15 10:30:00. Supón que la función está en auditoria.py.

22.18 Solución posible del ejercicio

Una solución con patch:

from datetime import datetime
from unittest.mock import patch

from auditoria import crear_evento_auditoria


def test_crear_evento_auditoria():
    fecha_fija = datetime(2026, 5, 15, 10, 30, 0)

    with patch("auditoria.uuid4") as uuid4_mock, patch("auditoria.datetime") as datetime_mock:
        uuid4_mock.return_value = "EVT-1"
        datetime_mock.now.return_value = fecha_fija

        evento = crear_evento_auditoria("USR-1")

    assert evento == {
        "id": "EVT-1",
        "usuario_id": "USR-1",
        "creado_en": "2026-05-15T10:30:00",
    }
    uuid4_mock.assert_called_once_with()
    datetime_mock.now.assert_called_once_with()

La prueba parchea los nombres usados por el módulo auditoria.

22.19 Conclusión

Fechas, horas, aleatoriedad e identificadores generados deben controlarse para que las pruebas sean repetibles. Podemos usar inyección de dependencias cuando diseñamos el código, o patch y monkeypatch cuando necesitamos reemplazar nombres existentes.

En el próximo tema veremos cómo mockear llamadas HTTP con respuestas controladas.