Fake timers do Vitest no Angular: o modelo mental depois do fakeAsync

O fakeAsync fazia dois trabalhos ao mesmo tempo: avançava o tempo virtual e drenava os microtasks de Promise. O Vitest separa os dois: o vi.useFakeTimers() controla o tempo, e você dá await nos microtasks. Quase todo teste de tempo no Angular sob Vitest é essa única ideia, aplicada.

O fakeAsync fazia dois trabalhos

O fakeAsync fundia duas responsabilidades num helper só. O tick(ms) adiantava timers, e também drenava callbacks de Promise pendentes. Por causa dessa fusão, um teste que esperava um .then() dentro de um setTimeout simplesmente funcionava; e é ela que faz a migração pro Vitest parecer um passo atrás no começo: o Vitest não junta os dois.

O Vitest te dá as duas metades separadas. O tempo virtual é vi.useFakeTimers() mais vi.advanceTimersByTime(ms). A metade dos microtasks é o seu próprio await. Guarda essa separação na cabeça e o resto da migração é mecânico: onde o fakeAsync fazia os dois de graça, agora você faz em dois passos.

O mesmo teste, antes e depoists
// O fakeAsync fazia os dois trabalhos numa chamada:
it('faz polling três vezes', fakeAsync(() => {
  const service = TestBed.inject(PollService);
  service.start();
  tick(3000);
  expect(service.count()).toBe(3);
}));

// Vitest: o tempo é explícito, e não há Promise aqui, então sem await.
it('faz polling três vezes', () => {
  vi.useFakeTimers();
  const service = TestBed.inject(PollService);
  service.start();
  vi.advanceTimersByTime(3000);
  expect(service.count()).toBe(3);
});

O mapa de API

A maior parte da conversão é consulta de tabela. O único ponto que exige julgamento é se há uma Promise na cadeia, o que decide entre o avanço síncrono e o assíncrono.

O que o fakeAsync faziaEquivalente no VitestQuando
tick(ms)vi.advanceTimersByTime(ms)Só timers, nada retorna Promise
tick(ms) com Promise na cadeiaawait vi.advanceTimersByTimeAsync(ms)Async validator, .then, whenStable
flush()vi.runAllTimers() / await vi.runAllTimersAsync()Drenar todos os timers pendentes de uma vez
discardPeriodicTasks()vi.clearAllTimers()Parar intervals antes de assertar
reset entre testesvi.useRealTimers() no afterEachSempre

A regra única que pega todo mundo

O vi.advanceTimersByTime() dispara timers. Ele não drena microtasks de Promise. Então no instante em que uma Promise entra na cadeia (um async validator, um .then, um whenStable), o avanço síncrono dispara o timer mas sua assertion roda antes do callback resolver. O control ainda está PENDING, o valor ainda é undefined, e a falha parece impossível.

A correção é uma substituição: await vi.advanceTimersByTimeAsync(ms), que avança o tempo e drena os microtasks. A regra de bolso é pequena o suficiente pra decorar: qualquer Promise na cadeia, use a variante ...Async e dê await. Essa regra única é a maior parte do que faz os testes de tempo do Vitest parecerem diferentes do fakeAsync.

Promise na cadeia precisa do avanço assíncronots
it('marca username em uso', async () => {
  vi.useFakeTimers();
  const control = new FormControl('taken', {
    asyncValidators: [uniqueUsernameValidator(['taken'])],
  });

  await vi.advanceTimersByTimeAsync(200);   // tempo E microtasks

  expect(control.status).toBe('INVALID');
});

As partes que não são timers

Três coisas do Angular parecem tempo mas não são, então apelar pro advanceTimersByTime não faz nada de útil.

Os schedulers do RxJS de fato pegam carona no timer. debounceTime, throttleTime e o async scheduler rodam no mesmo setInterval que o vi controla, então avançar o tempo dispara eles sem tratamento especial. Essa parte é fácil.

Effects de signal não. Um effect é processado durante o change detection, não num relógio, então avançar o tempo nunca o roda. Dirija com TestBed.tick() (TestBed.flushEffects() está deprecado). E o change detection ainda precisa rodar: avançar o tempo pode atualizar um signal, mas um componente OnPush só renderiza depois de detectChanges().

Um effect é processado pelo change detection, não por timerts
const service = TestBed.inject(ActivityLogService);

TestBed.tick();                 // processa o effect, sem timer envolvido
service.status.set('busy');
TestBed.tick();
expect(service.history()).toEqual(['idle', 'busy']);

Quando não apelar pros fake timers

Fake timers nem sempre são a ferramenta certa. Para um stream com muito tempo, um teste marble modela a linha do tempo direto e nunca toca em Zone.js ou fake timers: cold('a-b-c-------|') por debounceTime(3) assere '-------c----|' no TestScheduler. Para várias emissões ao longo do tempo, isso costuma ser mais claro que avançar um relógio na mão.

E se nada agenda um timer, você não precisa de fake timers. Um teste de Promise pura só precisa ser async e dar await no valor. Fake timers merecem espaço só quando há tempo virtual a controlar.

Esse é o modelo inteiro: controle o tempo com vi.useFakeTimers(), drene microtasks com await, processe effects com TestBed.tick(), e deixe os testes marble cuidarem dos streams com muito tempo. O erro ProxyZone e suas reescritas são esse modelo aplicado a um teste quebrado; se vale migrar uma suíte é a decisão acima dele.

Artefato para reaproveitar

Qual ferramenta de tempo para qual teste

  • Só timers, nada retorna Promise: vi.advanceTimersByTime(ms).
  • Promise em qualquer ponto da cadeia (async validator, .then, whenStable): await vi.advanceTimersByTimeAsync(ms).
  • Um effect de signal: TestBed.tick(), não um timer.
  • Um componente OnPush: chame detectChanges() depois de avançar o tempo, para ele renderizar.
  • Um stream RxJS com muito tempo: um teste marble no TestScheduler, sem fake timers.
  • Uma Promise pura sem timer: faça o teste async e dê await; pule fake timers.
  • Sempre: vi.useRealTimers() no afterEach.

Perguntas frequentes

Qual a diferença entre vi.advanceTimersByTime e vi.advanceTimersByTimeAsync?

O síncrono vi.advanceTimersByTime(ms) dispara só timers. O assíncrono vi.advanceTimersByTimeAsync(ms) também drena os microtasks de Promise entre eles. Use a variante assíncrona sempre que houver uma Promise na cadeia (um async validator, um .then, um whenStable), ou sua assertion roda antes do resultado ser aplicado.

Por que o tick() do fakeAsync resolvia timers e Promises?

Porque o fakeAsync fundia dois trabalhos: avançar o tempo virtual e drenar os microtasks pendentes. O tick(ms) fazia os dois. O Vitest separa, então sob o novo runner você avança o tempo e drena os microtasks em dois passos, o segundo sendo um await.

Como processo um effect de signal com fake timers do Vitest?

Você não usa timers para effects. Um effect de signal é processado durante o change detection, não num relógio, então avançar o tempo não faz nada por ele. Chame TestBed.tick() (o TestBed.flushEffects() existe mas está deprecado).

Preciso de fake timers para um teste marble do RxJS?

Não. O TestScheduler modela a linha do tempo sozinho e não depende de Zone.js nem de fake timers, então testes marble seguem funcionando sem mudança. Para streams com várias emissões ao longo do tempo, costumam ser mais claros que avançar um relógio na mão.

Quando devo evitar fake timers por completo?

Quando nada agenda um timer. Se um teste só espera uma Promise sem setTimeout ou interval por baixo, faça-o async e dê await no valor. Fake timers compensam só quando há tempo virtual a controlar.

Fontes consultadas

Modern Angular Playbook

Este artigo é um play.

O Playbook do Angular Moderno reúne o diagnóstico, a matriz de adoção, onze jogadas e o plano de 30 dias. Grátis, em inglês e português.

Você recebe os dois PDFs por email, pela lista do Dojo IA.

Abrir playbook →

André Ramosdisponível para vagas remotas, UTC−3Entre em contato →

Leia também

Guia · 9 min

fakeAsync e o erro ProxyZone: consertando testes Angular no Vitest

Por que fakeAsync, tick e waitForAsync lançam o erro ProxyZone no runner Vitest do Angular, e as duas correções: o patch experimental de zone testing, ou a reescrita para fake timers do Vitest.

Ler artigo →

Guia · 6 min

Quando migrar uma suíte Angular para Vitest

Vitest virou o test runner padrão do Angular CLI, mas migrar uma suíte existente ainda é experimental: quais specs migrar primeiro, o que o fakeAsync quebra, e como manter o CI confiável.

Ler artigo →

Guia · 10 min

O código que quebra quando Angular fica zoneless

Um guia técnico para revisar timers, subscriptions, forms, callbacks de terceiros e testes antes de um spike zoneless no Angular.

Ler artigo →