O teste chama setTimeout, espera um segundo e fica verde. Na semana seguinte, ele demora mais, falha no CI ou passa só quando é repetido. O problema não é o tempo de espera em si. É deixar o relógio do sistema decidir quando a asserção vai acontecer.

Para testar código dependente do tempo em JavaScript, separe duas coisas: a data que a regra consulta e os timers que agendam trabalho. Uma data fixa basta para testar vencimento ou expiração. Fake timers servem para avançar setTimeout, setInterval e animações sem dormir durante o teste. Em ambos os casos, a suíte precisa restaurar o relógio e esvaziar o trabalho assíncrono de forma explícita.

Se você ainda está escolhendo a camada certa para cada comportamento, a visão geral dos tipos de testes no desenvolvimento de software ajuda a separar unidade, integração e E2E antes de controlar o relógio.

Este guia usa Jest, Vitest, node:test e Playwright para comparar as fronteiras. Os trechos de código são ilustrativos e não foram executados neste repositório. Os comandos de verificação mostram como adaptar cada exemplo à sua suíte.

Diagrama mostra um teste trocando o relógio real por um relógio controlado para verificar datas e timers.

Resposta curta

  • Use uma data fixa quando o código só precisa saber “que horas são”.
  • Use fake timers quando o comportamento depende de callbacks, intervalos ou atrasos.
  • Para funções assíncronas, avance timers e a fila de Promises antes de verificar o resultado.
  • Restaure timers reais no fim do teste. Um relógio vazando contamina casos que parecem não ter relação.

Qual parte do tempo o código realmente usa?

Comece nomeando a dependência. Date.now() e new Date() representam o relógio consultado pela regra. setTimeout e setInterval representam trabalho que deve acontecer depois. performance.now() mede duração. O relógio de uma página também pode controlar requestAnimationFrame e outros APIs do navegador.

Essas dependências parecem iguais quando um teste falha por “tempo”, mas pedem provas diferentes. Uma função que classifica um token como expirado pode receber um instante como argumento. Um mecanismo de retry precisa provar que o segundo attempt só começa depois do atraso. Uma tela que atualiza um contador precisa de um clock instalado no BrowserContext.

Antes de escolher uma API do runner, escreva a pergunta em uma frase:

  1. “Dado este instante, o resultado da regra é X?”
  2. “Depois de avançar 500 milissegundos, o callback foi chamado?”
  3. “Depois de dois intervalos, a interface mostra o estado correto?”

Se a resposta for a primeira, prefira injetar o instante ou congelar a data. Se for a segunda, controle os timers. Se for a terceira, use o relógio do navegador e instale-o antes de carregar a página.

Quando uma data fixa é melhor que fake timers?

Uma data fixa é a opção mais simples quando a regra não agenda nada. O teste precisa saber se uma assinatura está ativa em um instante, não simular o passar de minutos. Nesse caso, congelar a data reduz a superfície do mock e deixa claro que a asserção é sobre calendário.

Quando a API aceita o instante como argumento, nem é necessário mockar o ambiente:

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

test("trata a assinatura como expirada no instante limite", () => {
  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);
});

Esse desenho costuma ser melhor para regras de domínio porque o teste não depende de Date.now(). Se a aplicação usa a hora atual em muitos pontos, crie uma pequena abstração, como clock.now(), e forneça uma implementação real em produção e uma implementação fixa no teste. O objetivo não é esconder o tempo. É tornar a fonte da decisão visível.

Quando você precisa controlar Date.now() sem disparar timers, vi.setSystemTime no Vitest e page.clock.setFixedTime no Playwright são exemplos de APIs que alteram a hora observada mantendo os timers em funcionamento. A documentação do Vitest, “Vi” e do Playwright, “Clock”, consultadas em 25/08/2026, distinguem esse caso de avançar a fila de timers.

Quando fake timers são a ferramenta certa?

Use fake timers quando o comportamento que você quer provar acontece depois de um atraso, em um intervalo ou em uma sequência de callbacks. O guia de Timer Mocks do Jest, consultado em 25/08/2026, mostra que o runner troca as funções nativas por versões cujo avanço pode ser controlado. O guia de timers do Vitest, consultado na mesma data, oferece a mesma ideia com vi.useFakeTimers().

O teste abaixo é ilustrativo. A função espera um atraso antes de chamar uma operação, e o caso verifica que não é preciso esperar de verdade:

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

test("avança o atraso sem dormir no teste", 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();
  }
});

O detalhe importante é advanceTimersByTimeAsync. Avançar o relógio pode executar o callback do timer, mas a Promise retornada pela operação ainda precisa de uma oportunidade para ser resolvida. O nome da API varia entre runners. No Jest, use a variante assíncrona correspondente quando o callback criar Promises; no node:test, use context.mock.timers.tick() para o timer e aguarde a operação separadamente.

Não use runAllTimers() em uma função que agenda um intervalo permanente ou um retry sem limite. O runner pode entrar em um ciclo até alcançar o limite de timers. Avance até o evento que interessa, faça a asserção e encerre o recurso.

Como escolher entre Jest, Vitest e node:test?

Escolha a API que já acompanha a suíte, não um runner novo só porque o nome do método parece mais conveniente. Os princípios são iguais, mas o ciclo de vida e o suporte a Promises mudam.

Runner Controlar a data Avançar timers Cuidado principal
Jest jest.setSystemTime() jest.advanceTimersByTime() ou variante assíncrona Chame jest.useRealTimers() no cleanup.
Vitest vi.setSystemTime() vi.advanceTimersByTimeAsync() O relógio falso precisa ser instalado antes do código agendar o timer.
node:test mock.timers.enable({ apis: ["Date"] }) context.mock.timers.tick(ms) A API depende da versão de Node usada pelo CI.

O guia de testes do Node.js, consultado em 25/08/2026, documenta mock.timers.enable, tick e reset. A versão que o seu CI executa é parte do contrato. Não copie um exemplo da documentação mais recente sem confirmar node --version no ambiente real.

Se a suíte usa Node sem Jest ou Vitest, node:test evita adicionar uma dependência somente para controlar timers. Se a equipe já usa Vitest ou Jest, trocar de runner não torna o teste mais determinístico. O que importa é saber qual relógio está falso, quem agenda o trabalho e quando a fila assíncrona foi consumida.

Como testar tempo em uma página Playwright?

Um fake timer do processo de teste não controla automaticamente o relógio dentro da página. Para comportamento de browser, use o clock do Playwright. A API page.clock pode controlar Date, timers, frames de animação e performance no BrowserContext. A documentação de Clock do Playwright, consultada em 25/08/2026, recomenda instalar o relógio antes da navegação quando o código agenda trabalho durante o carregamento.

O trecho é ilustrativo:

test("mostra a sessão expirada depois de avançar o 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");
});

Use setFixedTime quando a página só precisa exibir uma data estável. Use runFor ou fastForward quando o timer deve disparar. O Playwright Clock separa setSystemTime, que muda a hora sem disparar timers, de métodos que avançam o relógio e executam callbacks.

Isso não substitui o teste do serviço que decide a expiração. A página pode mostrar a mensagem correta e ainda enviar um timestamp incorreto para a API. Mantenha a regra de domínio coberta em uma camada mais rápida e use o teste Playwright para verificar a experiência que só existe no browser. Para outras fronteiras de uma suíte E2E, veja o artigo sobre estado compartilhado em testes Playwright no CI. Se a dúvida for a rede, o artigo sobre mockar APIs no Playwright sem falsos positivos cobre uma fronteira diferente.

Por que testes com fake timers continuam falhando?

Fake timers controlam uma fonte de não determinismo, não todas elas. Os problemas mais comuns são estes:

  • O relógio foi instalado tarde demais: o módulo capturou setTimeout no import e ficou apontando para a implementação real. Instale o clock antes de importar ou reorganize a dependência.
  • A Promise ficou pendente: o timer avançou, mas o callback iniciou uma operação assíncrona. Use a API assíncrona do runner e aguarde o resultado.
  • O timer vazou para o próximo caso: restaure timers reais e limpe intervalos em afterEach ou finally.
  • A data e o fuso foram misturados: compare instantes em UTC quando essa é a regra. Não use uma string local para esconder uma conversão de timezone.
  • O código usa outro relógio: performance.now(), process.hrtime() e APIs do browser podem precisar de configuração própria. Confirme o que o runner realmente intercepta.
  • A espera mascara uma corrida: trocar await new Promise(resolve => setTimeout(resolve, 100)) por um timer falso pode deixar o teste rápido, mas não corrige uma asserção que não espera uma condição observável.

No Playwright, não transforme page.waitForTimeout() em estratégia de sincronização. A documentação da API Page, consultada em 25/08/2026, trata a espera fixa como inadequada para estabilizar testes. Espere uma condição que represente o comportamento ou controle o clock quando o tempo for exatamente o objeto da prova.

Como verificar que o teste mede a regra certa?

Antes de colocar um caso temporal no CI, revise a fronteira com este checklist:

  1. A asserção é sobre a data observada, sobre um timer disparado ou sobre os dois?
  2. O código agenda o timer depois que o relógio falso foi instalado?
  3. O teste avança exatamente até o evento que quer provar?
  4. Promises e callbacks foram aguardados depois do avanço?
  5. O caso restaura timers, datas e intervalos mesmo quando falha?
  6. O fuso horário está explícito na fixture?
  7. A regra de domínio está testada sem depender do browser ou da rede?
  8. A execução ainda falha quando você remove a asserção que deveria proteger?

Rode o caso isolado, depois a suíte completa e, por fim, uma execução repetida com a mesma configuração do CI. Se o teste só passa isolado, procure vazamento de clock ou ordem de execução. Se só passa com tempo real, o exemplo pode estar dependendo de uma fila que o teste não drena. Se o teste precisa de uma API real, separe essa integração da prova determinística do scheduler.

Testes temporais não precisam ser lentos nem mágicos. Dê um nome à parte do tempo que importa, controle essa parte com a API do runner e preserve a fronteira que ficou de fora. A suíte então responde uma pergunta concreta: qual estado deveria existir neste instante, e qual trabalho deveria ter sido executado até aqui?