---
title: "Mockando serviços e HttpClient no Angular com Vitest: vi.fn, spies e DI"
description: "O mapa de spies de Jasmine para Vitest nos testes Angular (vi.fn, vi.spyOn, mockReturnValue), por que um mock por DI vence o vi.mock para serviços, e como o HttpTestingController segue funcionando com provideHttpClientTesting."
deck: "[Vitest é o test runner padrão do Angular desde a v21](https://angular.dev/guide/testing) (estável, novembro/2025). A primeira coisa que você pega depois da migração é o spy, e os nomes mudaram: `jasmine.createSpy` virou [`vi.fn`](https://vitest.dev/api/vi), `spyOn` virou `vi.spyOn`. Mas o mock idiomático no Angular nunca foi o spy. É um fake entregue pela [DI](https://angular.dev/guide/testing/services)."
author: "André Ramos"
url: "https://andreramos.dev/pt/angular/mocking-angular-services-vitest/"
lang: "pt-BR"
type: "article"
---

# Mockando serviços e HttpClient no Angular com Vitest: vi.fn, spies e DI

> [Vitest é o test runner padrão do Angular desde a v21](https://angular.dev/guide/testing) (estável, novembro/2025). A primeira coisa que você pega depois da migração é o spy, e os nomes mudaram: `jasmine.createSpy` virou [`vi.fn`](https://vitest.dev/api/vi), `spyOn` virou `vi.spyOn`. Mas o mock idiomático no Angular nunca foi o spy. É um fake entregue pela [DI](https://angular.dev/guide/testing/services).

## Os nomes dos spies mudaram, a ideia não

O primeiro teste que você abre depois de migrar para Vitest costuma ser o que tem um spy dentro, e o spy não compila. O `jasmine.createSpy` sumiu, o `spyOn(...).and.returnValue(...)` sumiu, o `expect(spy).toHaveBeenCalledTimes` continua igual mas o `spy.calls.count()` não. O reflexo é achar que o framework de teste ficou mais difícil. Não ficou. A maior parte da superfície é um rename de um para um, e o schematic `refactor-jasmine-vitest` do Angular 22 faz a passada mecânica por você.

O guia oficial de migração nomeia as conversões direto: ele `Replaces jasmine.createSpy with vi.fn`, `Converts spyOn calls to the equivalent vi.spyOn`, e `Replaces jasmine.objectContaining with expect.objectContaining`. Leia a tabela abaixo como o mapa que o schematic segue, e a próxima seção como a única decisão que o schematic não consegue tomar por você.

| Jasmine | Vitest | Nota |
|---|---|---|
| `jasmine.createSpy('name')` | `vi.fn()` | Uma função mock isolada, com rastreio de chamadas. |
| `jasmine.createSpyObj('svc', ['load'])` | `{ load: vi.fn() }` | Um objeto literal de `vi.fn()`s. Sem factory. |
| `spyOn(obj, 'method')` | `vi.spyOn(obj, 'method')` | Default diferente: veja a próxima seção. |
| `.and.returnValue(v)` | `.mockReturnValue(v)` | `.mockReturnValueOnce(v)` para uma chamada só. |
| `.and.resolveTo(v)` | `.mockResolvedValue(v)` | Retorna uma Promise resolvida. |
| `.and.callFake(fn)` | `.mockImplementation(fn)` | Troca o corpo pelo seu. |
| `.and.callThrough()` | (default do `vi.spyOn`) | O `vi.spyOn` já chama o método real. |
| `.and.stub()` | `.mockImplementation(() => {})` | O Jasmine stuba por padrão; o Vitest não. |
| `spy.calls.count()` | `spy.mock.calls.length` | Array dos arrays de argumentos por chamada. |
| `spy.calls.argsFor(0)` | `spy.mock.calls[0]` | Argumentos da primeira chamada. |
| `spy.calls.mostRecent().args` | `spy.mock.lastCall` | Argumentos da última chamada. |
| `spy.calls.reset()` | `spy.mockClear()` | Limpa o histórico, mantém a implementação. |
| `jasmine.any(String)` | `expect.any(String)` | Matcher assimétrico nas assertions. |

## O único rename que muda comportamento

Trocar `spyOn` por `vi.spyOn` parece um rename puro. É a única linha daquela tabela que muda o que o teste faz. A estratégia padrão de spy do Jasmine é `and.stub()`: um `spyOn(service, 'save')` simples troca o método por um no-op que retorna `undefined`. O `vi.spyOn` do Vitest faz o contrário por padrão. Ele envolve o método real e chama ele de verdade, então o código original continua rodando, a menos que você adicione `.mockReturnValue` ou `.mockImplementation`.

Essa diferença é silenciosa e morde. Um teste Jasmine que espionava o `save()` para impedir uma chamada HTTP real de disparar vai, depois de um rename literal para `vi.spyOn`, deixar essa chamada disparar de novo. O teste pode até passar enquanto bate num backend real ou estoura fundo dentro de uma dependência que o teste nunca quis exercitar. Essa é a decisão que o schematic não consegue tomar: ele converte a sintaxe, mas não tem como saber que você contava com o stub padrão do Jasmine.

Então, quando reviso um diff de `spyOn` para `vi.spyOn`, a única pergunta que faço é: esse spy queria suprimir o método real? Se a resposta é sim, a conversão está incompleta até ter um `.mockReturnValue(...)` ou `.mockImplementation(() => {})` explícito. Eu não daria merge num `vi.spyOn` cru em cima de um método com efeito colateral.

*Mesma intenção, três spies do Vitest*

```ts
import { vi } from 'vitest';

// 1. Fake isolado (era jasmine.createSpy): retorna o que você mandar.
const load = vi.fn().mockReturnValue(of([{ id: 1 }]));

// 2. Spy que SUPRIME o método real (default do Jasmine; do Vitest não).
const svc = TestBed.inject(OrdersService);
vi.spyOn(svc, 'save').mockResolvedValue({ ok: true });

// 3. Spy que só OBSERVA e deixa o método real rodar (default do Vitest).
const audit = vi.spyOn(svc, 'audit');
svc.confirm();
expect(audit).toHaveBeenCalledWith('confirm');
```

## O mock idiomático no Angular é um provider de DI, não um spy

Spy é a resposta quando você quer observar ou dobrar um método de um objeto que você já tem em mãos. É a ferramenta errada para substituir um colaborador inteiro. No Angular, a unidade sob teste recebe os colaboradores pelo injector, então o lugar natural de colocar um fake é o mesmo: o array de providers. Você entrega ao `TestBed` um objeto fake e manda usar ele no lugar do serviço real.

O fake é um objeto comum cujos métodos são `vi.fn()`s. Você não roda `vi.spyOn` numa instância real, não constrói o serviço de verdade e não arrasta junto nenhuma dependência transitiva. O par `provide`/`useValue` é a mesma forma que o guia de testes do Angular mostra para stubar uma dependência, e soa familiar para quem escreve teste Angular desde 2017. O `vi.fn()` só entra no lugar do `jasmine.createSpy()` lá dentro.

Eu prefiro isso ao mock no nível do módulo para qualquer serviço que o componente ou o serviço sob teste injeta, porque mocka na fronteira que a aplicação de fato usa (o injector), em vez de passar por cima dela direto no sistema de módulos. E mantém o fake tipado contra a interface real, então um rename de método quebra o teste em vez de passar quieto.

*Serviço fake pela DI, métodos são vi.fn()*

```ts
import { vi } from 'vitest';
import { TestBed } from '@angular/core/testing';

// Um fake tipado: cada método é um vi.fn(). Tipado contra o serviço real,
// então uma mudança de assinatura em OrdersService falha aqui em vez de passar.
const ordersMock: Partial<Record<keyof OrdersService, ReturnType<typeof vi.fn>>> = {
  load: vi.fn().mockReturnValue(of([{ id: 1, total: 90 }])),
  save: vi.fn().mockResolvedValue({ ok: true }),
};

beforeEach(() => {
  TestBed.configureTestingModule({
    providers: [{ provide: OrdersService, useValue: ordersMock }],
  });
});

it('carrega pedidos no init', () => {
  const facade = TestBed.inject(OrdersFacade);
  facade.init();
  expect(ordersMock.load).toHaveBeenCalledOnce();
});
```

## HttpClient: o backend de teste não mudou

A preocupação mais comum depois da troca de runner são os testes de HTTP, e é a mais deslocada. O `provideHttpClientTesting` e o `HttpTestingController` são APIs do Angular, não do Karma. Eles nunca dependeram do test runner, então funcionam no Vitest exatamente como funcionavam no Karma. Nada na camada que casa as requisições mudou.

Para um teste de HTTP simples, o único provider que você precisa é o `provideHttpClientTesting()`. No momento em que você testa configuração do cliente (um interceptor, por exemplo) você adiciona o `provideHttpClient(...)` também, e a ordem importa: a doc é explícita que o `provideHttpClient()` precisa vir antes do `provideHttpClientTesting()`, porque o provider de teste sobrescreve partes do real. O `HttpTestingController` injetado te dá `expectOne`, `match`, `expectNone`, o `flush` e o `error` da requisição, e o `verify` para garantir que nenhuma requisição ficou sem resposta.

Eu mantenho o `httpTesting.verify()` no `afterEach` de todo spec de HTTP. É a única assertion que pega uma requisição que o componente disparou e o teste esqueceu de esperar, que é justamente o tipo de regressão que um serviço mockado esconderia.

*Teste de HttpClient no Vitest: as mesmas APIs do Karma*

```ts
import { TestBed } from '@angular/core/testing';
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting, HttpTestingController } from '@angular/common/http/testing';

let httpTesting: HttpTestingController;

beforeEach(() => {
  TestBed.configureTestingModule({
    // provideHttpClient(...) primeiro, depois o provider de teste sobrescreve o backend.
    providers: [provideHttpClient(), provideHttpClientTesting()],
  });
  httpTesting = TestBed.inject(HttpTestingController);
});

afterEach(() => httpTesting.verify()); // nenhuma requisição sem resposta

it('faz GET do pedido e mapeia', () => {
  const service = TestBed.inject(OrdersService);
  let result: Order | undefined;
  service.load(1).subscribe((o) => (result = o));

  const req = httpTesting.expectOne('/api/orders/1');
  expect(req.request.method).toBe('GET');
  req.flush({ id: 1, total: 90 });

  expect(result?.total).toBe(90);
});
```

## Quando o vi.mock é certo, e por que ele briga com a DI

O `vi.mock` substitui os exports de um módulo no arquivo inteiro. É a ferramenta certa quando aquilo que você precisa fingir não é injetado: um import ES cru que o seu código chama direto, um SDK de terceiros sem provider Angular, um helper de `Date` ou `crypto` importado como função. Para esses, a DI não tem costura, e o `vi.mock` é a única costura que existe.

É a ferramenta errada para um serviço Angular injetado, e o motivo é mecânico, não estético. O Vitest iça todo `vi.mock` para o topo do arquivo, acima dos seus imports, então a factory roda antes de qualquer setup de teste e não enxerga um valor do `TestBed` nem uma variável definida depois (é para isso que o `vi.hoisted` existe). Ele troca a classe que o módulo exporta, mas o Angular não resolve um serviço importando a classe dele na hora da chamada; ele resolve o provider registrado no injector. Então um `vi.mock` do arquivo do serviço pode deixar o componente ainda recebendo o provider real, e você depura um mock que parece instalado mas nunca foi aplicado.

Para um serviço injetado, eu recusaria o `vi.mock` e usaria o provider de DI toda vez. Só recorro ao `vi.mock` quando não há costura de injector para usar, e mesmo aí mantenho a superfície mockada tão pequena quanto o teste precisa, porque um mock de módulo içado é a coisa mais fácil de vazar para o próximo teste numa suíte Vitest.

*vi.mock para import sem DI; provider de DI para o serviço*

```ts
import { vi } from 'vitest';

// Uso CERTO: um módulo de terceiros que o código importa direto, sem costura de DI.
// Içado acima dos imports, então é trocado antes do código sob teste carregar.
vi.mock('nanoid', () => ({ nanoid: () => 'test-id-123' }));

// Uso ERRADO para um serviço injetado: isso troca o export da classe, mas o
// componente segue recebendo o provider que o injector tem. Prefira:
TestBed.configureTestingModule({
  providers: [{ provide: OrdersService, useValue: { load: vi.fn() } }],
});
```

## Clear, reset, restore: escolha um e configure

Spies vazam entre testes quando ninguém os reseta, e o Vitest te dá três verbos fáceis de confundir. O `mockClear` apaga o histórico de chamadas e mantém a implementação. O `mockReset` apaga o histórico e devolve a implementação para um `vi.fn()` vazio. O `mockRestore` faz o reset e recoloca o método original no lugar, então só significa alguma coisa para spies criados com `vi.spyOn`.

Em vez de chamar isso na mão em todo `afterEach`, defina a política uma vez nas opções do test target. O `restoreMocks: true` roda o equivalente a `vi.restoreAllMocks()` depois de cada teste, que tanto reseta o histórico quanto desfaz o patch de todo `vi.spyOn`, então um spy de um spec não consegue vazar para o próximo. Esse é o ajuste que eu quero ligado por padrão, porque um spy vazado é um teste instável esperando para ser culpado por outra coisa.

| Chamada | Limpa histórico | Implementação depois | Use quando |
|---|---|---|---|
| `mockClear()` | Sim | Mantida | Reusar o mesmo fake entre `it`s de um bloco. |
| `mockReset()` | Sim | `vi.fn()` vazio | Cada teste define o próprio valor de retorno. |
| `mockRestore()` | Sim | Original de volta | Você usou `vi.spyOn` e quer o método real de volta. |
| `restoreMocks: true` (config) | Sim | Original de volta | O padrão global; ligue e pare de resetar na mão. |

## Artefato reaproveitável: Cheat sheet de spy Jasmine para Vitest

- `jasmine.createSpy()` vira `vi.fn()`; `jasmine.createSpyObj('s', ['a','b'])` vira `{ a: vi.fn(), b: vi.fn() }`.
- `spyOn(o, 'm')` vira `vi.spyOn(o, 'm')`, mas adicione `.mockImplementation(() => {})` ou `.mockReturnValue(...)` se o método tem efeito colateral: o Jasmine stuba por padrão, o Vitest chama o real.
- `.and.returnValue` vira `.mockReturnValue`; `.and.resolveTo` vira `.mockResolvedValue`; `.and.callFake` vira `.mockImplementation`.
- `spy.calls.count()` vira `spy.mock.calls.length`; `spy.calls.argsFor(0)` vira `spy.mock.calls[0]`; `spy.calls.reset()` vira `spy.mockClear()`.
- Substitua um serviço injetado inteiro com `{ provide: Service, useValue: { method: vi.fn() } }`, não com `vi.mock` do módulo do serviço.
- Mantenha `provideHttpClientTesting()` e `HttpTestingController` como estão; são independentes do runner. Adicione `provideHttpClient()` antes dele ao testar config do cliente.
- Use `vi.mock` só para imports sem DI (SDKs de terceiros, imports de função crus); ele é içado acima dos imports e passa por cima do injector.
- Ligue `restoreMocks: true` no test target para os spies não vazarem entre testes.

## Perguntas frequentes

### O que substitui jasmine.createSpy e spyOn nos testes Angular com Vitest?

O `jasmine.createSpy()` vira `vi.fn()`, e o `spyOn(obj, 'method')` vira `vi.spyOn(obj, 'method')`. Os métodos também mudam de nome: `.and.returnValue` vira `.mockReturnValue`, `.and.callFake` vira `.mockImplementation`, e o `spy.calls.count()` vira `spy.mock.calls.length`. O schematic `refactor-jasmine-vitest` do Angular 22 aplica a maioria disso automaticamente.

### Por que meu teste se comporta diferente depois de trocar spyOn por vi.spyOn?

Porque os padrões diferem. O `spyOn` do Jasmine stuba o método (um no-op que retorna `undefined`) por padrão. O `vi.spyOn` do Vitest chama o método real por padrão. Se o spy original existia para impedir um efeito colateral como uma chamada HTTP, adicione `.mockReturnValue(...)` ou `.mockImplementation(() => {})` depois do `vi.spyOn`, ou o método real roda.

### Devo mockar um serviço Angular com vi.mock ou com um provider de DI?

Use um provider de DI para qualquer serviço que a unidade sob teste injeta: `{ provide: Service, useValue: { method: vi.fn() } }`. O `vi.mock` é içado acima dos imports e troca o export de classe do módulo, que o injector do Angular não lê na hora da chamada, então ele costuma deixar o provider real no lugar. Reserve o `vi.mock` para imports sem DI, como SDKs de terceiros.

### O provideHttpClientTesting e o HttpTestingController ainda funcionam no Vitest?

Sim, sem mudança. São APIs do Angular e nunca dependeram do Karma. Use `provideHttpClientTesting()` sozinho para um teste de HTTP simples, ou `provideHttpClient(...)` antes do `provideHttpClientTesting()` quando você também testa configuração do cliente. Injete o `HttpTestingController` e use `expectOne`, `flush` e `verify` exatamente como antes.

### Como impedir os mocks do Vitest de vazar entre testes?

Ligue `restoreMocks: true` nas opções do test target, que roda `vi.restoreAllMocks()` depois de cada teste: limpa o histórico de chamadas e desfaz o patch de todo `vi.spyOn`. Para controle mais fino, chame `mockClear()` para manter a implementação, `mockReset()` para descartá-la, ou `mockRestore()` para recolocar o método original.

## Fontes consultadas

- https://angular.dev/guide/testing
- https://angular.dev/guide/testing/services
- https://angular.dev/guide/http/testing
- https://angular.dev/guide/testing/migrating-to-vitest
- https://angular.dev/api/common/http/testing/HttpTestingController
- https://angular.dev/api/common/http/testing/provideHttpClientTesting
- https://vitest.dev/api/vi
- https://vitest.dev/api/mock
- https://vitest.dev/guide/mocking
- https://blog.angular.dev/announcing-angular-v21-57946c34f14b

## Leia também

- [De Karma para Vitest no Angular: a config que realmente roda](https://andreramos.dev/pt/angular/karma-to-vitest-angular-config/)
- [Quando migrar uma suíte Angular para Vitest](https://andreramos.dev/pt/angular/move-angular-test-suite-to-vitest/)
- [fakeAsync e o erro ProxyZone: consertando testes Angular no Vitest](https://andreramos.dev/pt/angular/angular-vitest-fakeasync-proxyzone-error/)
