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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
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.
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.
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.
date.today() dentro de reglas puras.
Construí con TDD una función puede_cancelar_reserva.
fecha_reserva y hoy como parámetros.python -m pytest después de cada regla.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.