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!