25. Manejo de fechas, tiempo y reglas de negocio sin perder determinismo

25.1 Objetivo del tema

En este tema veremos cómo trabajar con fechas y tiempo en TDD sin crear pruebas frágiles. Las reglas que dependen del día actual, vencimientos o promociones pueden volverse difíciles de probar si el código consulta directamente el reloj del sistema.

Vamos a construir reglas prácticas usando datetime.date y pytest, manteniendo las pruebas determinísticas.

25.2 El problema del tiempo real

Una prueba determinística debe dar el mismo resultado cada vez que se ejecuta. Si una prueba depende de date.today(), puede pasar hoy y fallar mañana sin que el código haya cambiado.

En TDD, el tiempo debe tratarse como un dato de entrada o como una dependencia controlada.

25.3 Ejemplo frágil

Supongamos una regla de promoción de fin de mes. Este test parece razonable, pero depende del día real de ejecución.

Ejemplo a evitar:

from datetime import date

from promociones import tiene_promocion_fin_de_mes


def test_tiene_promocion_fin_de_mes():
    assert tiene_promocion_fin_de_mes(date.today())

Esta prueba solo pasará si se ejecuta en una fecha de fin de mes. No describe un ejemplo controlado.

25.4 Primera regla con fecha explícita

Escribimos una prueba con una fecha concreta. El 31 de mayo de 2026 es una fecha estable para el ejemplo.

Archivo a crear: tests/test_promociones.py

from datetime import date

from promociones import tiene_promocion_fin_de_mes


def test_hay_promocion_el_ultimo_dia_del_mes():
    fecha = date(2026, 5, 31)

    assert tiene_promocion_fin_de_mes(fecha)

Ejecutamos python -m pytest. La prueba fallará porque todavía no existe la función.

25.5 Implementación mínima

Creamos una primera implementación simple.

Archivo a crear: src/promociones.py

def tiene_promocion_fin_de_mes(fecha):
    return True

Esto alcanza para la primera prueba, pero todavía no prueba el caso contrario.

25.6 Segunda regla: no todos los días tienen promoción

Agregamos una prueba para una fecha que no corresponde al último día del mes.

Archivo a modificar: tests/test_promociones.py

def test_no_hay_promocion_en_un_dia_intermedio():
    fecha = date(2026, 5, 15)

    assert not tiene_promocion_fin_de_mes(fecha)

Esta prueba obliga a implementar la regla real.

25.7 Implementar fin de mes

Podemos calcular el día siguiente y verificar si cambia el mes.

Archivo a modificar: src/promociones.py

from datetime import timedelta


def tiene_promocion_fin_de_mes(fecha):
    dia_siguiente = fecha + timedelta(days=1)

    return dia_siguiente.month != fecha.month

Ejecutamos la suite. Ahora el comportamiento depende de la fecha que pasamos, no del reloj del sistema.

25.8 Probar meses con distinta duración

Las reglas de fecha suelen tener bordes. Febrero, meses de 30 días y meses de 31 días son buenos ejemplos para probar.

Archivo a modificar: tests/test_promociones.py

import pytest


@pytest.mark.parametrize("fecha", [
    date(2026, 2, 28),
    date(2026, 4, 30),
    date(2026, 12, 31),
])
def test_hay_promocion_en_ultimos_dias_de_distintos_meses(fecha):
    assert tiene_promocion_fin_de_mes(fecha)

La parametrización permite cubrir varios bordes sin repetir la misma estructura de prueba.

25.9 Regla con vencimiento

Ahora construimos otra regla: una factura está vencida si la fecha actual es posterior a su fecha de vencimiento.

Archivo a crear: tests/test_facturas.py

from datetime import date

from facturas import esta_vencida


def test_factura_esta_vencida_si_hoy_es_posterior_al_vencimiento():
    vencimiento = date(2026, 5, 10)
    hoy = date(2026, 5, 11)

    assert esta_vencida(vencimiento, hoy)

La prueba pasa el valor de hoy explícitamente. No deja que la función lo busque por su cuenta.

25.10 Implementar vencimiento

La implementación queda directa.

Archivo a crear: src/facturas.py

def esta_vencida(vencimiento, hoy):
    return hoy > vencimiento

Este diseño es fácil de probar porque todas las fechas relevantes son entradas visibles.

25.11 Borde: el mismo día del vencimiento

Definimos una regla importante: el día del vencimiento todavía no está vencida.

Archivo a modificar: tests/test_facturas.py

def test_factura_no_esta_vencida_el_mismo_dia_del_vencimiento():
    vencimiento = date(2026, 5, 10)
    hoy = date(2026, 5, 10)

    assert not esta_vencida(vencimiento, hoy)

Esta prueba documenta una decisión de negocio que podría ser distinta en otro sistema.

25.12 Evitar llamadas ocultas al reloj

Una implementación menos conveniente sería esta:

from datetime import date


def esta_vencida(vencimiento):
    return date.today() > vencimiento

El problema es que la función decide internamente qué significa hoy. Eso complica las pruebas y mezcla la regla del negocio con una dependencia externa.

25.13 Cuando el caso de uso necesita la fecha actual

A veces sí necesitamos usar la fecha real, pero podemos hacerlo en el borde externo del sistema y mantener puro el dominio.

Archivo a crear: src/casos_de_uso.py

from datetime import date

from facturas import esta_vencida


def verificar_factura_vencida(factura):
    hoy = date.today()

    return esta_vencida(factura.vencimiento, hoy)

Esta función de aplicación consulta el reloj. La regla pura sigue estando en esta_vencida.

25.14 Inyectar un proveedor de fecha

Si queremos probar el caso de uso sin depender del reloj real, podemos recibir una función que devuelva la fecha actual.

Archivo a modificar: src/casos_de_uso.py

from datetime import date

from facturas import esta_vencida


def verificar_factura_vencida(factura, obtener_hoy=date.today):
    hoy = obtener_hoy()

    return esta_vencida(factura.vencimiento, hoy)

El valor por defecto permite usar la fecha real en producción, pero las pruebas pueden controlar el tiempo.

25.15 Probar el caso de uso con fecha controlada

La prueba pasa una función que devuelve una fecha fija.

Archivo a crear: tests/test_casos_de_uso.py

from dataclasses import dataclass
from datetime import date

from casos_de_uso import verificar_factura_vencida


@dataclass
class Factura:
    vencimiento: date


def test_verifica_factura_vencida_con_fecha_controlada():
    factura = Factura(vencimiento=date(2026, 5, 10))

    resultado = verificar_factura_vencida(
        factura,
        obtener_hoy=lambda: date(2026, 5, 11)
    )

    assert resultado

La prueba es estable porque no importa en qué día se ejecute realmente.

25.16 Reglas con cantidad de días

Otra regla común es calcular días restantes. Primero definimos el caso normal.

Archivo a crear: tests/test_suscripciones.py

from datetime import date

from suscripciones import dias_restantes


def test_calcula_dias_restantes_hasta_el_vencimiento():
    hoy = date(2026, 5, 10)
    vencimiento = date(2026, 5, 15)

    assert dias_restantes(vencimiento, hoy) == 5

25.17 Implementar diferencia de días

La resta de fechas devuelve un timedelta.

Archivo a crear: src/suscripciones.py

def dias_restantes(vencimiento, hoy):
    return (vencimiento - hoy).days

Esta función sigue siendo determinística porque recibe ambas fechas.

25.18 Regla para suscripción vencida

Si la fecha de vencimiento ya pasó, podemos decidir que los días restantes sean cero.

Archivo a modificar: tests/test_suscripciones.py

def test_dias_restantes_no_puede_ser_negativo():
    hoy = date(2026, 5, 20)
    vencimiento = date(2026, 5, 15)

    assert dias_restantes(vencimiento, hoy) == 0

La prueba expresa una regla de negocio. Matemáticamente la diferencia sería negativa, pero para el usuario no queremos mostrar días negativos.

25.19 Implementar límite a cero

Ajustamos la función para respetar la regla.

Archivo a modificar: src/suscripciones.py

def dias_restantes(vencimiento, hoy):
    diferencia = (vencimiento - hoy).days

    if diferencia < 0:
        return 0

    return diferencia

Ejecutamos python -m pytest. La suite debe quedar en verde.

25.20 Buenas prácticas con fechas en TDD

  • Pasar fechas explícitas a funciones de dominio.
  • Usar fechas fijas en las pruebas.
  • Probar bordes: mismo día, día anterior, día posterior y fin de mes.
  • Evitar date.today() dentro de reglas puras.
  • Inyectar un proveedor de fecha cuando un caso de uso necesita el tiempo actual.

25.21 Errores frecuentes

  • Escribir pruebas que dependen del día real.
  • Mezclar reglas de negocio con lectura directa del reloj del sistema.
  • No probar bordes de calendario.
  • Usar fechas dinámicas cuando una fecha fija sería más clara.
  • Ocultar el concepto de "hoy" dentro de funciones difíciles de controlar.

25.22 Ejercicio práctico

Construí con TDD una función puede_cancelar_reserva.

  1. La reserva puede cancelarse si faltan 2 días o más para la fecha reservada.
  2. No puede cancelarse si falta 1 día.
  3. No puede cancelarse el mismo día.
  4. La función debe recibir fecha_reserva y hoy como parámetros.
  5. Ejecutá python -m pytest después de cada regla.

25.23 Checklist del tema

  • Las pruebas usan fechas concretas y repetibles.
  • Las reglas de dominio no consultan directamente el reloj.
  • Los casos de uso pueden recibir un proveedor de fecha.
  • Los bordes de calendario están representados por pruebas claras.
  • El tiempo se trata como dato, no como sorpresa escondida.

25.24 Conclusión

Las reglas que dependen del tiempo no tienen por qué generar pruebas frágiles. Si pasamos fechas explícitas, controlamos el reloj desde las pruebas y mantenemos la lógica de dominio separada de la entrada o salida, el diseño queda más simple y confiable.

En el próximo tema veremos cómo diseñar límites entre lógica de dominio y entrada o salida.