La prueba llama a setTimeout, espera un segundo y queda en verde. La semana siguiente tarda más, falla en CI o solo pasa después de repetirla. El problema no es que el código use tiempo. Es que el reloj del sistema decide cuándo llega la aserción.

Para probar código dependiente del tiempo en JavaScript, separa dos dependencias: la fecha que lee la regla y los timers que programan el trabajo. Una fecha fija basta para una regla de expiración. Los fake timers permiten avanzar setTimeout, setInterval y callbacks de animación sin dormir durante la prueba. En ambos casos, la suite debe restaurar el reloj y vaciar el trabajo asíncrono de forma explícita.

Si todavía estás eligiendo la capa correcta para cada comportamiento, la visión general de tipos de pruebas de software ayuda a separar las preguntas unitarias, de integración y E2E antes de controlar el reloj.

Esta guía compara Jest, Vitest, node:test y Playwright. Los ejemplos de código son ilustrativos y no se ejecutaron en este repositorio. Los comandos de verificación muestran cómo adaptarlos a tu propia suite.

Diagrama que muestra una prueba sustituyendo el reloj real por uno controlado para verificar fechas y timers.

Respuesta corta

  • Usa una fecha fija cuando el código solo necesita saber “qué hora es”.
  • Usa fake timers cuando el comportamiento depende de callbacks, intervalos o retrasos.
  • En funciones asíncronas, avanza los timers y la cola de Promises antes de comprobar el resultado.
  • Restaura los timers reales al terminar. Un reloj filtrado contamina casos que parecen no tener relación.

¿Qué parte del tiempo usa realmente el código?

Empieza por nombrar la dependencia. Date.now() y new Date() representan el reloj que consulta la regla. setTimeout y setInterval representan el trabajo que debe ocurrir después. performance.now() mide duración. Un reloj del navegador también puede controlar requestAnimationFrame y otras APIs.

Estas dependencias parecen iguales cuando una prueba falla por “tiempo”, pero necesitan pruebas distintas. Una función que marca un token como expirado necesita un instante conocido. Un mecanismo de reintento debe demostrar que el segundo intento empieza después del retraso. Una página que actualiza un contador necesita un clock instalado en su BrowserContext.

Antes de elegir una API del runner, escribe la pregunta en una frase:

  1. “Dado este instante, ¿el resultado de la regla es X?”
  2. “Después de avanzar 500 milisegundos, ¿se llamó al callback?”
  3. “Después de dos intervalos, ¿la interfaz muestra el estado correcto?”

Si la respuesta es la primera, inyecta o congela el instante. Si es la segunda, controla los timers. Si es la tercera, usa el reloj del navegador e instálalo antes de cargar la página.

¿Cuándo es mejor una fecha fija que fake timers?

Una fecha fija es la opción más sencilla cuando la regla no programa trabajo. La prueba necesita saber si una suscripción está activa en un instante, no simular que pasan minutos. Congelar la fecha mantiene pequeño el mock y deja claro que la aserción trata sobre el calendario.

Cuando la API acepta el instante como argumento, no necesitas mockear el entorno:

export function isExpired(expiresAt: Date, now: Date): boolean {
  return now.getTime() >= expiresAt.getTime();
}

test("trata la suscripción como expirada en el límite", () => {
  const expiresAt = new Date("2026-08-25T12:00:00.000Z");
  const now = new Date("2026-08-25T12:00:00.000Z");

  expect(isExpired(expiresAt, now)).toBe(true);
});

Este diseño suele ser mejor para las reglas de dominio porque la prueba no depende de Date.now(). Si la aplicación consulta la hora actual en muchos puntos, crea una abstracción pequeña como clock.now(), con una implementación real en producción y otra fija en las pruebas. La meta no es ocultar el tiempo, sino hacer visible el origen de la decisión.

Cuando necesitas controlar Date.now() sin disparar timers, vi.setSystemTime en Vitest y page.clock.setFixedTime en Playwright son ejemplos de APIs que cambian la hora observada mientras los timers siguen funcionando. La documentación de Vitest, “Vi” y Playwright, “Clock”, consultada el 25/08/2026, distingue este caso de avanzar la cola de timers.

¿Cuándo son adecuados los fake timers?

Usa fake timers cuando el comportamiento que quieres probar ocurre después de un retraso, dentro de un intervalo o en una secuencia de callbacks. La guía de Timer Mocks de Jest, consultada el 25/08/2026, describe cómo sustituir las funciones nativas por versiones cuyo avance se puede controlar. La guía de timers de Vitest, consultada el mismo día, ofrece la misma idea mediante vi.useFakeTimers().

El siguiente ejemplo es ilustrativo. La función espera antes de llamar a una operación, y la prueba demuestra que no necesita dormir:

export async function retryAfter<T>(
  operation: () => Promise<T>,
  delayMs: number,
): Promise<T> {
  await new Promise<void>((resolve) => setTimeout(resolve, delayMs));
  return operation();
}

test("avanza el retraso sin dormir", async () => {
  vi.useFakeTimers();

  try {
    const operation = vi.fn().mockResolvedValue("ok");
    const result = retryAfter(operation, 1_000);

    expect(operation).not.toHaveBeenCalled();
    await vi.advanceTimersByTimeAsync(1_000);

    await expect(result).resolves.toBe("ok");
    expect(operation).toHaveBeenCalledTimes(1);
  } finally {
    vi.useRealTimers();
  }
});

El detalle importante es advanceTimersByTimeAsync. Avanzar el reloj puede ejecutar el callback del timer, pero la Promise que devuelve la operación aún necesita una oportunidad para resolverse. El nombre de la API cambia entre runners. En Jest, usa la variante asíncrona equivalente cuando el callback cree Promises. En node:test, usa context.mock.timers.tick() para el timer y espera la operación por separado.

No uses runAllTimers() en una función que programa un intervalo permanente o un retry sin límite. El runner puede entrar en un ciclo hasta alcanzar el límite de timers. Avanza hasta el evento que importa, haz la aserción y cierra el recurso.

¿Cómo elegir entre Jest, Vitest y node:test?

Elige la API que ya usa la suite, no un runner nuevo porque el nombre de un método parezca más cómodo. Los principios son iguales, pero el ciclo de vida y el soporte de Promises cambian.

Runner Controlar la fecha Avanzar timers Precaución principal
Jest jest.setSystemTime() jest.advanceTimersByTime() o su variante asíncrona Llama a jest.useRealTimers() en la limpieza.
Vitest vi.setSystemTime() vi.advanceTimersByTimeAsync() Instala el reloj falso antes de que el código programe el timer.
node:test mock.timers.enable({ apis: ["Date"] }) context.mock.timers.tick(ms) La API depende de la versión de Node usada por CI.

La documentación del test runner de Node.js, consultada el 25/08/2026, documenta mock.timers.enable, tick y reset. La versión que ejecuta tu CI forma parte del contrato. No copies un ejemplo de la documentación más reciente sin comprobar node --version en el entorno real.

Si la suite usa Node sin Jest ni Vitest, node:test evita añadir una dependencia solo para controlar timers. Si el equipo ya usa Vitest o Jest, cambiar de runner no vuelve determinista la prueba. Lo importante es saber qué reloj es falso, quién programó el trabajo y cuándo se consumió el trabajo asíncrono.

¿Cómo probar el tiempo dentro de una página de Playwright?

Un fake timer en el proceso de la prueba no controla automáticamente el reloj dentro de la página. Para el comportamiento del navegador, usa el clock de Playwright. La API page.clock puede controlar Date, timers, frames de animación y performance en el BrowserContext. La API Clock de Playwright, consultada el 25/08/2026, recomienda instalar el clock antes de la navegación cuando la aplicación programa trabajo durante la carga.

El ejemplo es ilustrativo:

test("muestra la sesión expirada después de avanzar el clock", async ({ page }) => {
  await page.clock.install({ time: new Date("2026-08-25T12:00:00.000Z") });
  await page.goto("/session");

  await page.clock.fastForward("30:00");
  await expect(page.getByRole("status")).toHaveText("Session expired");
});

Usa setFixedTime cuando la página solo necesita una fecha estable. Usa runFor o fastForward cuando un timer deba dispararse. La API Clock de Playwright separa setSystemTime, que cambia la hora sin disparar timers, de los métodos que avanzan el reloj y ejecutan callbacks.

Esto no sustituye la prueba del servicio que decide la expiración. La página puede mostrar el mensaje correcto y aun así enviar un timestamp incorrecto a la API. Mantén la regla de dominio en una capa más rápida y usa Playwright para el comportamiento que solo existe en el navegador. Para otra frontera E2E, consulta cómo evitar estado compartido entre pruebas Playwright en CI. Si la duda está en la red, cómo mockear APIs en Playwright sin falsos positivos cubre una frontera distinta.

¿Por qué siguen fallando las pruebas con fake timers?

Los fake timers controlan una fuente de no determinismo, no todas. Los problemas más comunes son estos:

  • El reloj se instaló demasiado tarde: un módulo capturó setTimeout al importarse y todavía apunta a la implementación real. Instala el reloj antes de importarlo o rediseña la dependencia.
  • La Promise quedó pendiente: el timer avanzó, pero el callback comenzó un trabajo asíncrono. Usa la API asíncrona del runner y espera el resultado.
  • El timer se filtró al caso siguiente: restaura los timers reales y limpia intervalos en afterEach o finally.
  • Se mezclaron fecha y zona horaria: compara instantes UTC cuando esa sea la regla. No uses una cadena local para ocultar una conversión de zona horaria.
  • El código usa otro reloj: performance.now(), process.hrtime() y APIs del navegador pueden necesitar su propia configuración. Confirma qué intercepta realmente el runner.
  • Una espera oculta una carrera: sustituir await new Promise(resolve => setTimeout(resolve, 100)) por un fake timer hace la prueba más rápida, pero no corrige una aserción que no espera una condición observable.

En Playwright, no conviertas page.waitForTimeout() en una estrategia de sincronización. La documentación de Page API, consultada el 25/08/2026, presenta las esperas fijas como inadecuadas para estabilizar pruebas. Espera una condición que represente el comportamiento o controla el clock cuando el tiempo sea exactamente el objeto de la prueba.

¿Cómo verificar que la prueba mide la regla correcta?

Antes de poner un caso temporal en CI, revisa su frontera con esta lista:

  1. ¿La aserción trata sobre una fecha observada, un timer disparado o ambos?
  2. ¿El código programa el timer después de instalar el reloj falso?
  3. ¿La prueba avanza exactamente hasta el evento que quiere demostrar?
  4. ¿Se esperan las Promises y los callbacks después de avanzar el tiempo?
  5. ¿El caso restaura timers, fechas e intervalos aunque falle?
  6. ¿La zona horaria está explícita en el fixture?
  7. ¿La regla de dominio se prueba sin el navegador ni la red?
  8. ¿La prueba falla si quitas la aserción que debería proteger el comportamiento?

Ejecuta el caso solo, luego la suite completa y por último una ejecución repetida con la configuración de CI. Si solo pasa de forma aislada, busca filtraciones del reloj u orden de ejecución. Si solo pasa con tiempo real, el ejemplo puede depender de una cola que la prueba nunca vacía. Si necesita una API real, separa esa integración de la prueba determinista del scheduler.

Las pruebas temporales no tienen que ser lentas ni mágicas. Nombra la parte del tiempo que importa, contrólala con la API del runner y conserva la frontera que no controlaste. La suite podrá responder una pregunta concreta: ¿qué estado debería existir en este instante y qué trabajo debería haberse ejecutado ya?