---
title: "fakeAsync e o erro ProxyZone: consertando testes Angular no Vitest"
description: "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."
deck: "[Vitest é o test runner padrão do Angular desde a v21](https://angular.dev/guide/testing), e a maioria dos specs vem junto sem reclamar. Os que mexem com tempo não: `fakeAsync`, `tick`, `flush` e `waitForAsync` lançam `Expected to be running in 'ProxyZone', but it was not found`. Existem duas correções suportadas, e o [guia oficial de migração](https://angular.dev/guide/testing/migrating-to-vitest) deixa claro qual ele prefere."
author: "André Ramos"
url: "https://andreramos.dev/pt/angular/angular-vitest-fakeasync-proxyzone-error/"
lang: "pt-BR"
type: "article"
---

# fakeAsync e o erro ProxyZone: consertando testes Angular no Vitest

> [Vitest é o test runner padrão do Angular desde a v21](https://angular.dev/guide/testing), e a maioria dos specs vem junto sem reclamar. Os que mexem com tempo não: `fakeAsync`, `tick`, `flush` e `waitForAsync` lançam `Expected to be running in 'ProxyZone', but it was not found`. Existem duas correções suportadas, e o [guia oficial de migração](https://angular.dev/guide/testing/migrating-to-vitest) deixa claro qual ele prefere.

## O erro, e por que ele acontece

Você troca o test runner do Angular para Vitest, roda a suíte, e a maior parte passa. Aí todo spec que controla tempo falha com a mesma mensagem: `Expected to be running in 'ProxyZone', but it was not found`. O stack aponta para `zone-testing.js`, não para o seu código.

A causa é bem específica. `fakeAsync`, `tick`, `flush` e `waitForAsync` não controlam tempo sozinhos. Eles delegam para uma Zone chamada ProxyZone, que o `zone.js/testing` instala em volta de cada spec. O setup do Karma fazia isso por você no `test.ts`. O builder do Vitest roda seus testes, mas não envolve cada um na ProxyZone, então o helper verifica que a zone existe e para antes do corpo do seu teste rodar.

Não é a lógica do seu teste falhando. O mesmo spec passou no Karma ontem. O que mudou foi o aparato de teste em volta dele, o que significa que a correção é de configuração ou reescrita, não caça a bug na assertion.

*O spec que passava no Karma, agora lançando o erro*

```ts
import { fakeAsync, tick } from '@angular/core/testing';

it('limpa o toast depois do timeout', fakeAsync(() => {
  let message = 'saving';
  setTimeout(() => (message = ''), 1000);
  tick(1000);
  expect(message).toBe('');
}));

// Error: Expected to be running in 'ProxyZone', but it was not found.
//   at assertPresent (node_modules/zone.js/fesm2015/zone-testing.js)
```

## Dois caminhos de saída, e qual o Angular recomenda

Existem exatamente duas respostas suportadas, e elas não são iguais. Você pode manter seus testes baseados em Zone.js vivos com um patch experimental, ou reescrevê-los para controlar tempo com os fake timers do próprio Vitest.

O guia oficial é explícito sobre a direção. Ele recomenda converter para `async` nativo e fake timers do Vitest, e chama isso de abordagem estabelecida. O patch existe para que uma suíte grande não trave o upgrade inteiro no primeiro dia, não como o lugar onde você quer ficar.

| Caminho | O que você muda | Melhor quando | Custo |
|---|---|---|---|
| Patch de zone testing | Adiciona o patch aos polyfills de teste e mantém `fakeAsync`/`tick` | Suíte grande que você não consegue reescrever em uma branch | Experimental, com duas arestas afiadas |
| Reescrita para timers do Vitest | Troca `fakeAsync` por `vi.useFakeTimers()` e avança o tempo você mesmo | Testes que você já está tocando, e todo teste novo | Uma passada mecânica por teste, mas durável |

## A saída de emergência: o patch de zone testing

Se você precisa da suíte verde hoje, o Angular traz um patch opt-in. Na configuração de build de teste, deixe os polyfills como `["zone.js", "zone.js/testing", "zone.js/plugins/vitest-patch"]`. A ordem importa: o `zone.js/testing` registra o `ProxyZoneSpec` que o patch envolve, então ele precisa carregar primeiro. Com a ordem errada, você troca o erro de ProxyZone por `Missing ProxyZoneSpec`.

A segunda aresta é mais silenciosa e me custou uma sessão real de debug. O patch envolve o `it`, o `describe` e o `beforeEach` globais do Vitest. Um spec que os importa com `import { it } from 'vitest'` se liga a outra função e pula o wrapper, então `fakeAsync` continua lançando mesmo com o patch instalado. Para usar o patch, a suíte tem que rodar nos globais do Vitest, não em funções de teste importadas.

Eu trato isso como paliativo. Ele carrega uma flag de experimental e vai contra a direção que o time Angular aponta, então recorro a ele pra desbloquear uma suíte grande e vou convertendo os testes conforme mexo neles.

*O mesmo patch, dois resultados*

```ts
// Patch instalado, mas isso AINDA lança: o `it` importado pula o wrapper.
import { it } from 'vitest';
it('salva', fakeAsync(() => { tick(1000); /* erro de ProxyZone */ }));

// Funciona: o `it` global é o que o patch envolveu (globais do Vitest).
it('salva', fakeAsync(() => {
  const fixture = TestBed.createComponent(SaveIndicatorComponent);
  fixture.componentInstance.save();
  tick(1000);
  expect(fixture.nativeElement.textContent).toContain('saved');
}));
```

## A reescrita, padrão por padrão

O caminho durável troca `fakeAsync` por fake timers do Vitest e avança o tempo você mesmo. A forma é sempre a mesma: `vi.useFakeTimers()` no topo, avança, verifica, e `vi.useRealTimers()` no `afterEach` para os timers não vazarem entre testes. A parte que vale saber é quais padrões escondem uma armadilha. O Angular 22 adicionou um schematic `refactor-jasmine-vitest --fake-async` que faz a versão mecânica disso por você, então leia as conversões abaixo como o que conferir na saída dele, e a armadilha do microtask como a decisão que ele não consegue tomar.

Um timer simples é mecânico. `vi.advanceTimersByTime(300)` o dispara de forma síncrona, e um `debounceTime` do RxJS pega carona no mesmo timer, porque o scheduler dele roda em `setInterval`.

A armadilha é uma Promise na cadeia: um async validator, um `.then`, um `whenStable`. Ali, o avanço síncrono dispara o timer mas não drena o microtask que aplica o resultado, então o control fica `PENDING` e a assertion falha por um motivo que não parece ter nada a ver com tempo. A correção é o avanço assíncrono, `await vi.advanceTimersByTimeAsync(...)`, que drena os dois.

Dois padrões não precisam de timer nenhum. Effects de signal são processados durante o change detection, então rode eles com `TestBed.tick()` (`TestBed.flushEffects()` está deprecado). Testes marble usam `TestScheduler`, que nunca dependeu de Zone.js, então seguem passando sem mudança.

*Timer e debounce do RxJS: o avanço síncrono*

```ts
afterEach(() => vi.useRealTimers());

it('debounce de termos rápidos até o último', () => {
  vi.useFakeTimers();
  const service = TestBed.inject(SearchService);
  const seen: string[] = [];
  service.results$.subscribe((t) => seen.push(t));

  service.search('a');
  service.search('ang');
  vi.advanceTimersByTime(300);

  expect(seen).toEqual(['ang']);
});
```

*A armadilha do microtask: Promise na cadeia precisa do avanço assíncrono*

```ts
// O advanceTimersByTime síncrono dispara o timer mas deixa o control PENDING,
// porque não drena o microtask que escreve o resultado da validação.
it('marca username em uso depois da checagem assíncrona', async () => {
  vi.useFakeTimers();
  const control = new FormControl('taken', {
    asyncValidators: [uniqueUsernameValidator(['taken', 'admin'])],
  });

  await vi.advanceTimersByTimeAsync(200);

  expect(control.status).toBe('INVALID');
  expect(control.errors).toEqual({ taken: true });
});
```

*Effects e marble: sem fakeAsync envolvido*

```ts
// Effects sao processados no change detection: dirija com TestBed.tick().
TestBed.tick();
expect(service.history).toEqual(['idle']);

// Testes marble rodam no TestScheduler, independente de Zone.js, então
// seguem funcionando sem mudanca. Para streams com muito tempo, fica mais limpo.
scheduler.run(({ cold, expectObservable }) => {
  const source = cold('-a-b-c|', { a: 1, b: 2, c: 3 });
  expectObservable(source.pipe(map((x) => x * 10))).toBe('-a-b-c|', { a: 10, b: 20, c: 30 });
});
```

## O que eu faria de verdade

A decisão é por teste, não por suíte. Um teste que você já está editando: reescreva. São poucas linhas, e tira uma dependência de Zone.js que você não vai sentir falta. Uma suíte grande que ninguém está tocando: instale o patch, mantenha o CI verde, e converta os testes conforme as features te trouxerem de volta a eles. Um teste novo: escreva com fake timers do Vitest desde o início e nunca encontre o erro de ProxyZone.

Isso fica um degrau abaixo da pergunta anterior, a de se vale migrar a suíte, tratada em [Quando migrar uma suíte Angular para Vitest](/pt/angular/move-angular-test-suite-to-vitest). E os padrões aqui são os mesmos que aparecem quando um app vira [zoneless](/pt/angular/code-that-breaks-when-angular-goes-zoneless), porque os dois removem a Zone que antes absorvia o tempo por você.

## Artefato reaproveitável: Checklist de conversão de fakeAsync para Vitest

- Confirme que a falha é o erro de ProxyZone, não uma assertion real: o mesmo spec passava no Karma.
- Escolha o caminho por teste: patch para suíte grande intocada, reescrita para os testes que você de fato toca.
- Para reescrever, troque `fakeAsync` por `vi.useFakeTimers()` e chame `vi.useRealTimers()` no `afterEach`.
- Se há uma Promise em qualquer ponto da cadeia (async validator, `.then`, `whenStable`), use `await vi.advanceTimersByTimeAsync()`: o avanço síncrono deixa o estado pendente.
- Em componentes OnPush, chame `detectChanges()` depois de avançar o tempo, já que o timer marca a view como suja mas não a renderiza.
- Dirija effects de signal com `TestBed.tick()`, e deixe os testes marble no `TestScheduler` sem mudança.
- Se optar pelo patch, ordene os polyfills `zone.js`, `zone.js/testing`, `zone.js/plugins/vitest-patch`, e rode nos globais do Vitest, não no `it` importado.

## Perguntas frequentes

### Por que meus testes Angular lançam "Expected to be running in 'ProxyZone'" depois de migrar para Vitest?

Porque `fakeAsync`, `tick`, `flush` e `waitForAsync` precisam de uma Zone chamada ProxyZone que o `zone.js/testing` instala em volta de cada spec, e o builder do Vitest não envolve seus testes nela por padrão. Os mesmos specs passavam no Karma porque o setup do Karma instalava essa zone. Você corrige adicionando o patch de zone testing aos polyfills de teste, ou reescrevendo os testes para usar fake timers do Vitest.

### Dá para continuar usando fakeAsync no Vitest?

Dá, com o patch experimental. Deixe os polyfills de build de teste como `["zone.js", "zone.js/testing", "zone.js/plugins/vitest-patch"]` nessa ordem, e garanta que a suíte usa o `it` e o `describe` globais do Vitest, não os importados, porque o patch envolve os globais. O Angular marca isso como experimental e recomenda converter para async nativo e timers do Vitest.

### O que substitui tick() e flush() em um teste Vitest?

Use `vi.useFakeTimers()` com `vi.advanceTimersByTime(ms)` para tempo síncrono, e `await vi.advanceTimersByTimeAsync(ms)` quando há uma Promise na cadeia. Chame `vi.useRealTimers()` no `afterEach` para os timers não vazarem entre testes.

### Meu async validator fica PENDING depois que avanço os timers. Por quê?

Porque o `vi.advanceTimersByTime` síncrono dispara o timer mas não drena o microtask que aplica o resultado da validação ao control. Use `await vi.advanceTimersByTimeAsync(ms)`, que drena o timer e o microtask, então o control resolve para VALID ou INVALID.

### Testes marble do RxJS ainda funcionam no Vitest?

Sim. O `TestScheduler` não depende de Zone.js nem de `fakeAsync`, então testes marble seguem funcionando sem mudança. Para streams com muito tempo, costumam ser mais limpos que fake timers.

## Fontes consultadas

- https://angular.dev/guide/testing
- https://angular.dev/guide/testing/migrating-to-vitest
- https://angular.dev/api/core/testing/fakeAsync
- 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

## Leia também

- [Quando migrar uma suíte Angular para Vitest](https://andreramos.dev/pt/angular/move-angular-test-suite-to-vitest/)
- [O código que quebra quando Angular fica zoneless](https://andreramos.dev/pt/angular/code-that-breaks-when-angular-goes-zoneless/)
- [O que adotar, testar ou deixar para depois no Angular 21](https://andreramos.dev/pt/angular/adopt-spike-wait-angular-21/)
