Tutoriales
Estrategias de Testing para Timestamps: Guía Completa
Introducción
Probar código que depende del tiempo es notoriamente difícil. Este tutorial te enseñará estrategias efectivas para escribir tests confiables para código relacionado con timestamps.
Lo que Aprenderás
- ✅ Por qué el testing de tiempo es difícil
- ✅ Mocking de tiempo en JavaScript/Jest
- ✅ Freezegun en Python
- ✅ Testing de transiciones DST
- ✅ Testing de múltiples zonas horarias
- ✅ Mejores prácticas
Por qué es Difícil
// Este test puede fallar aleatoriamente
test('el evento expira después de 1 hora', () => {
const evento = crearEvento();
// ❌ Depende del tiempo actual
expect(evento.expirado()).toBe(false);
});
Problemas:
- El tiempo actual cambia constantemente
- DST puede afectar resultados
- Tests pueden fallar cerca de medianoche
Mocking en JavaScript (Jest)
Fake Timers
describe('Expiración de eventos', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
test('el evento expira después de 1 hora', () => {
// Fijar tiempo a una fecha específica
jest.setSystemTime(new Date('2024-01-15T10:00:00Z'));
const evento = crearEvento({ duracion: 60 * 60 * 1000 });
expect(evento.expirado()).toBe(false);
// Avanzar tiempo 30 minutos
jest.advanceTimersByTime(30 * 60 * 1000);
expect(evento.expirado()).toBe(false);
// Avanzar 31 minutos más (total 61 min)
jest.advanceTimersByTime(31 * 60 * 1000);
expect(evento.expirado()).toBe(true);
});
});
Mock de Date.now()
test('timestamp de creación', () => {
const ahora = 1705312800000; // 2024-01-15T10:00:00Z
const espia = jest.spyOn(Date, 'now').mockReturnValue(ahora);
const registro = crearRegistro();
expect(registro.creadoEn).toBe(ahora);
espia.mockRestore();
});
Freezegun en Python
Básico
from freezegun import freeze_time
from datetime import datetime
@freeze_time("2024-01-15 10:00:00")
def test_timestamp_creacion():
registro = crear_registro()
assert registro.creado_en == datetime(2024, 1, 15, 10, 0, 0)
@freeze_time("2024-01-15 10:00:00")
def test_expiracion():
evento = crear_evento(duracion_segundos=3600)
assert not evento.expirado()
# Mover tiempo hacia adelante
with freeze_time("2024-01-15 11:01:00"):
assert evento.expirado()
Con Zonas Horarias
from freezegun import freeze_time
from datetime import datetime
from zoneinfo import ZoneInfo
@freeze_time("2024-01-15 10:00:00", tz_offset=-5)
def test_con_zona_horaria():
utc = ZoneInfo('UTC')
ny = ZoneInfo('America/New_York')
ahora_utc = datetime.now(utc)
ahora_ny = datetime.now(ny)
assert ahora_utc.hour == 15 # 10 AM UTC + offset simulado
Testing de DST
JavaScript
describe('Transiciones DST', () => {
test('Spring Forward - hora faltante', () => {
// 31 marzo 2024, 2:00 AM no existe en Madrid
jest.setSystemTime(new Date('2024-03-31T00:30:00Z'));
const evento = programarEvento('Europe/Madrid', '02:30');
// Debe ajustar automáticamente
expect(evento.horaLocal).toBe('03:30');
});
test('Fall Back - hora duplicada', () => {
// 27 octubre 2024, 2:30 AM ocurre dos veces
jest.setSystemTime(new Date('2024-10-27T00:30:00Z'));
const evento = programarEvento('Europe/Madrid', '02:30');
// Debe elegir la segunda ocurrencia (después del cambio)
expect(evento.offset).toBe('+01:00');
});
});
Python
from freezegun import freeze_time
from datetime import datetime
from zoneinfo import ZoneInfo
def test_spring_forward():
"""Test hora faltante durante DST"""
madrid = ZoneInfo('Europe/Madrid')
# Justo antes del cambio
with freeze_time("2024-03-31 00:59:00"):
antes = datetime.now(madrid)
# Justo después del cambio
with freeze_time("2024-03-31 03:01:00"):
despues = datetime.now(madrid)
# Verificar que el offset cambió
assert antes.strftime('%z') == '+0100'
assert despues.strftime('%z') == '+0200'
def test_fall_back():
"""Test hora duplicada durante DST"""
madrid = ZoneInfo('Europe/Madrid')
# Primera ocurrencia de 2:30 (CEST)
primera = datetime(2024, 10, 27, 2, 30, fold=0, tzinfo=madrid)
# Segunda ocurrencia de 2:30 (CET)
segunda = datetime(2024, 10, 27, 2, 30, fold=1, tzinfo=madrid)
# Mismo tiempo local, diferente offset
assert primera.strftime('%z') == '+0200'
assert segunda.strftime('%z') == '+0100'
Testing de Múltiples Zonas Horarias
import pytest
from datetime import datetime
from zoneinfo import ZoneInfo
ZONAS_HORARIAS_TEST = [
'UTC',
'America/New_York',
'Europe/Madrid',
'Asia/Tokyo',
'Australia/Sydney',
]
@pytest.mark.parametrize('zona', ZONAS_HORARIAS_TEST)
def test_conversion_zona_horaria(zona):
"""Verifica conversión correcta en todas las zonas"""
utc = ZoneInfo('UTC')
tz = ZoneInfo(zona)
# Crear timestamp en UTC
dt_utc = datetime(2024, 6, 15, 12, 0, 0, tzinfo=utc)
# Convertir a zona local
dt_local = dt_utc.astimezone(tz)
# Verificar que representan el mismo instante
assert dt_utc.timestamp() == dt_local.timestamp()
Mejores Prácticas
1. Aísla la Obtención de Tiempo
// ❌ Difícil de probar
function crearEvento() {
return {
creadoEn: Date.now(),
// ...
};
}
// ✅ Fácil de probar
function crearEvento(obtenerTiempo = Date.now) {
return {
creadoEn: obtenerTiempo(),
// ...
};
}
2. Usa Interfaces de Reloj
# Interfaz de reloj inyectable
class Reloj:
def ahora(self):
return datetime.now(timezone.utc)
class RelojFijo:
def __init__(self, tiempo_fijo):
self._tiempo = tiempo_fijo
def ahora(self):
return self._tiempo
# En tests
def test_con_reloj_fijo():
reloj = RelojFijo(datetime(2024, 1, 15, 10, 0, tzinfo=timezone.utc))
servicio = MiServicio(reloj=reloj)
# ...
3. Prueba Casos Límite
FECHAS_LIMITE_TEST = [
datetime(2024, 1, 1, 0, 0, 0), # Año nuevo
datetime(2024, 2, 29, 12, 0, 0), # Año bisiesto
datetime(2024, 3, 31, 2, 30, 0), # Spring Forward
datetime(2024, 10, 27, 2, 30, 0), # Fall Back
datetime(2024, 12, 31, 23, 59, 59), # Fin de año
]
Resumen
- Usa fake timers en Jest para controlar el tiempo
- Usa freezegun en Python para congelar el tiempo
- Prueba explícitamente transiciones DST
- Prueba con múltiples zonas horarias
- Inyecta la obtención de tiempo para facilitar testing
¡Prueba nuestro Convertidor Unix Timestamp para verificar tus cálculos!