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.
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:
- “Dado este instante, ¿el resultado de la regla es X?”
- “Después de avanzar 500 milisegundos, ¿se llamó al callback?”
- “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ó
setTimeoutal 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
afterEachofinally. - 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:
- ¿La aserción trata sobre una fecha observada, un timer disparado o ambos?
- ¿El código programa el timer después de instalar el reloj falso?
- ¿La prueba avanza exactamente hasta el evento que quiere demostrar?
- ¿Se esperan las Promises y los callbacks después de avanzar el tiempo?
- ¿El caso restaura timers, fechas e intervalos aunque falle?
- ¿La zona horaria está explícita en el fixture?
- ¿La regla de dominio se prueba sin el navegador ni la red?
- ¿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?