Modern Angular
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 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 fazia | Equivalente no Vitest | Quando |
|---|---|---|
tick(ms) | vi.advanceTimersByTime(ms) | Só timers, nada retorna Promise |
tick(ms) com Promise na cadeia | await 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 testes | vi.useRealTimers() no afterEach | Sempre |
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.
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().
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
asynce dêawait; pule fake timers. - Sempre:
vi.useRealTimers()noafterEach.
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
- https://angular.dev/guide/testing
- https://angular.dev/guide/testing/migrating-to-vitest
- https://angular.dev/api/core/testing/TestBed
- https://vitest.dev/api/vi
- https://vitest.dev/guide/mocking
- https://blog.angular.dev/announcing-angular-v21-57946c34f14b
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.
André Ramosdisponível para vagas remotas, UTC−3Entre em contato →