Vitest: setup, matchers, mocks, spies, coverage
6 min de leitura
O vitest é o test runner padrão em React em 2026.
Funciona com Vite, Next, Remix, TanStack Start, ou
qualquer projeto TS. A API lembra Jest (porque é
inspirada em Jest), mas é mais rápido (ESM nativo,
não transpila), tem type-checking integrado, e
suporta import direto. Este nó cobre o mínimo
viável: setup, matchers, mocks, spies, coverage.
Se você entende o ciclo describe / it / expect
e como mockar uma dependência, o resto da API vira
"combinações dessas peças".
O essencial 🟢
Setup em 3 linhas. Pra rodar Vitest num projeto Vite/Next, instala e roda:
pnpm add -D vitest @vitest/ui
// vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
environment: "jsdom", // ou "happy-dom" - engine de DOM pro Node
globals: true, // describe/it/expect globais (sem import)
setupFiles: ["./vitest.setup.ts"],
},
});
// vitest.setup.ts (carregado antes de cada teste)
import "@testing-library/jest-dom/vitest"; // matchers extras de DOM
// package.json
{
"scripts": {
"test": "vitest",
"test:run": "vitest run", // CI mode - roda uma vez e sai
"coverage": "vitest run --coverage"
}
}
vitest (sem run) fica em watch mode -
re-roda os testes a cada mudança de arquivo.
vitest run é o que CI usa - roda uma vez e
termina. vitest --ui abre uma interface web
no browser com a árvore de testes.
describe, it/test, expect - a anatomia
do teste. Todo teste segue o mesmo esqueleto:
import { describe, it, expect } from "vitest";
import { somar, dividir } from "./math";
describe("math", () => {
// Agrupa testes relacionados (aninhável)
it("somar dois números positivos", () => {
// Caso de teste
expect(somar(1, 2)).toBe(3);
});
it("somar com zero", () => {
expect(somar(0, 5)).toBe(5);
});
describe("dividir", () => {
it("divide números válidos", () => {
expect(dividir(10, 2)).toBe(5);
});
it("joga erro ao dividir por zero", () => {
// Para testar throw
expect(() => dividir(10, 0)).toThrow("Divisão por zero");
});
});
});
describe(name, fn)- agrupa testes. Pode ser aninhado (describe dentro de describe). Bom pra organizar "todas as funções de validação", "todos os hooks de form", etc.it(name, fn)outest(name, fn)- sinônimos.ité mais comum em BDD;testé mais direto. Use o que o time prefere.expect(value)- retorna um objeto com matchers.expect(x).toBe(y),toEqual(obj),toThrow(), etc.
Os matchers mais usados. Vitest herda matchers
do expect do Jest (mesma API), e adiciona
matchers extras via expect.extend():
// Igualdade
expect(1 + 1).toBe(2); // Object.is (estrito, sem coerção)
expect({ a: 1 }).toEqual({ a: 1 }); // recursivo (deep equal)
expect({ a: 1 }).toStrictEqual({ a: 1 }); // recursivo + tipos
// Booleanos
expect(true).toBeTruthy();
expect(false).toBeFalsy();
expect(null).toBeNull();
expect(undefined).toBeUndefined();
// Números
expect(0.1 + 0.2).toBeCloseTo(0.3); // ignora erro de ponto flutuante
expect(5).toBeGreaterThan(3);
expect(5).toBeLessThanOrEqual(5);
// Strings
expect("Aprenda").toMatch(/enda/);
expect("Aprenda").toContain("enda");
// Arrays / Iteráveis
expect([1, 2, 3]).toContain(2);
expect([1, 2, 3]).toHaveLength(3);
expect(new Set([1, 2])).toContain(1);
// Objetos
expect({ a: 1, b: 2 }).toHaveProperty("a");
expect({ a: 1, b: 2 }).toMatchObject({ a: 1 });
// Exceções
expect(() => JSON.parse("x")).toThrow();
expect(() => JSON.parse("x")).toThrow(SyntaxError);
expect(() => JSON.parse("x")).toThrow(/JSON/); // match na mensagem
// Promises
await expect(fetch("/api")).resolves.toEqual({ id: 1 });
await expect(fetch("/api")).rejects.toThrow("404");
// Funções (mocks, aprofundamento abaixo)
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledWith("arg1", "arg2");
expect(mockFn).toHaveBeenCalledTimes(3);
toBe vs toEqual confunde no começo. Resumo:
toBe-Object.is. Compara primitivos ou referência.expect(1).toBe(1)✅,expect({a:1}).toBe({a:1})❌ (referências diferentes).toEqual- deep equal recursivo. Compara estrutura.expect({a:1}).toEqual({a:1})✅.toStrictEqual- deep equal + tipo estrito.undefinedvs ausente não passa (mais rigoroso quetoEqual).
Mocks com vi.fn(). O vi é o namespace
de mock do Vitest (análogo ao jest do Jest).
vi.fn() cria uma função mock - você controla
o que ela retorna e inspeciona como foi chamada:
import { vi, describe, it, expect } from "vitest";
it("chama callback ao clicar", () => {
const onClick = vi.fn(); // mock que retorna undefined
const botao = render(<Botao onClick={onClick}>Bla</Botao>);
botao.click();
expect(onClick).toHaveBeenCalledTimes(1);
expect(onClick).toHaveBeenCalledWith(expect.any(Object));
});
// Mock que retorna valor específico
const getUser = vi.fn().mockReturnValue({ id: 1, nome: "Ana" });
expect(getUser()).toEqual({ id: 1, nome: "Ana" });
// Mock que muda retorno por chamada
const randomMock = vi.fn()
.mockReturnValueOnce("primeiro")
.mockReturnValueOnce("segundo")
.mockReturnValue("padrão");
expect(randomMock()).toBe("primeiro");
expect(randomMock()).toBe("segundo");
expect(randomMock()).toBe("padrão");
vi.fn() é o canivete suíço de mock. Use pra:
substituir dependências, simular retorno de API,
verificar que função foi chamada, contar chamadas.
Mock de módulo com vi.mock(). Pra mockar
um módulo inteiro (não só uma função):
// userService.ts
export async function buscarUsuario(id: string) {
const r = await fetch(`/api/usuarios/${id}`);
return r.json();
}
// userService.test.ts
import { vi, describe, it, expect } from "vitest";
// Mock do módulo - substitui o módulo inteiro
vi.mock("./userService", () => ({
buscarUsuario: vi.fn().mockResolvedValue({ id: "1", nome: "Ana" }),
}));
import { buscarUsuario } from "./userService";
it("usa o mock do módulo", async () => {
const user = await buscarUsuario("1");
expect(user).toEqual({ id: "1", nome: "Ana" });
});
vi.mock() é hoisted pelo Vitest - mesmo que
você escreva depois do import, o mock é
aplicado antes do módulo ser carregado. É
o que destrava import { buscarUsuario } from "./userService" e o buscarUsuario ser o mock.
vi.spyOn() pra observar função existente.
Quando você quer manter a implementação original
mas observar como foi chamada:
import * as consoleMod from "console";
import { vi, describe, it, expect } from "vitest";
it("loga mensagem de erro", () => {
const spy = vi.spyOn(consoleMod, "error").mockImplementation(() => {});
// mockImplementation substitui a função - sem ela, ainda chama o original
minhaFuncaoQueLoga();
expect(spy).toHaveBeenCalledWith("erro esperado");
spy.mockRestore(); // restaura a implementação original
});
vi.spyOn é o caminho pra "espionar sem mudar".
O mockRestore() no final é importante pra não
vazar o spy pra outros testes.
Setup e teardown com beforeEach/afterEach.
Pra código que se repete entre testes:
import { beforeEach, afterEach, describe, it, expect } from "vitest";
describe("Carrinho", () => {
let carrinho: Carrinho;
beforeEach(() => {
// Roda antes de cada teste deste describe
carrinho = new Carrinho();
});
afterEach(() => {
// Roda depois de cada teste
localStorage.clear();
});
it("adiciona item", () => {
carrinho.adicionar({ id: "1", nome: "Camisa", preco: 50 });
expect(carrinho.itens).toHaveLength(1);
});
it("calcula total", () => {
carrinho.adicionar({ id: "1", nome: "Camisa", preco: 50 });
carrinho.adicionar({ id: "2", nome: "Calça", preco: 100 });
expect(carrinho.total()).toBe(150);
});
});
beforeAll/afterAll rodam uma vez antes/depois
de todos os testes do describe. beforeEach/
afterEach rodam antes/depois de cada teste.
Coverage com --coverage. Pra medir cobertura
de linhas/branches/funções:
pnpm add -D @vitest/coverage-v8
pnpm coverage
// vitest.config.ts
export default defineConfig({
test: {
coverage: {
provider: "v8", // ou "istanbul"
reporter: ["text", "html", "json"],
include: ["src/**/*.{ts,tsx}"],
exclude: ["**/*.test.{ts,tsx}", "**/*.spec.{ts,tsx}"],
thresholds: {
// Falha o build se cobertura ficar abaixo disso
branches: 70,
functions: 70,
lines: 70,
statements: 70,
},
},
},
});
thresholds é o pulo do gato: define um mínimo de
cobertura que falha o CI se não for atingido.
Em time que está construindo a cultura de teste,
é o "remédio" pra evitar regressão de cobertura.
Aprofundamento 🟡
Testando hooks React com renderHook. Hooks
são funções que precisam estar dentro de um
componente pra rodar. O renderHook do
@testing-library/react resolve:
import { renderHook, act } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import { useContador } from "./useContador";
describe("useContador", () => {
it("inicia com valor inicial", () => {
const { result } = renderHook(() => useContador(0));
expect(result.current.valor).toBe(0);
});
it("incrementa o valor", () => {
const { result } = renderHook(() => useContador(0));
act(() => {
result.current.incrementar();
});
expect(result.current.valor).toBe(1);
});
});
renderHook retorna { result } - o result.current
é o valor de retorno do hook no último render.
act() envolve a ação que dispara re-render,
garantindo que o React processou a atualização antes
do expect.
Testando funções async com timers falsos.
Pra testar setTimeout, setInterval, debounce:
import { vi, describe, it, expect } from "vitest";
describe("debounce", () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it("chama função apenas após 100ms sem chamadas", () => {
const fn = vi.fn();
const debounced = debounce(fn, 100);
debounced();
debounced();
debounced();
expect(fn).not.toHaveBeenCalled();
vi.advanceTimersByTime(100);
expect(fn).toHaveBeenCalledTimes(1);
});
});
vi.useFakeTimers() substitui setTimeout/Date
por versões controláveis. vi.advanceTimersByTime(100)
avança o relógio 100ms - tudo que estava agendado
roda. Sem isso, você teria que esperar 100ms reais
em cada teste (lento) ou usar done() + setTimeout
(frágil).
Testando promises com vi.waitFor. Pra esperar
algo acontecer (DOM atualizar, fetch resolver):
import { vi } from "vitest";
it("mostra nome do usuário após fetch", async () => {
render(<Perfil userId="1" />);
// Tela mostra "Carregando..." inicialmente
expect(screen.getByText("Carregando...")).toBeInTheDocument();
// Espera o nome aparecer (até 1s, re-checa a cada 50ms)
await vi.waitFor(
() => {
expect(screen.getByText("Ana")).toBeInTheDocument();
},
{ timeout: 1000, interval: 50 }
);
});
vi.waitFor é o caminho padrão pra esperar
condição assíncrona em teste. Alternativa do
Testing Library é findBy*, que veremos no nó 3.
Snapshot testing: existe, evite pra component. Snapshot é "salva a saída do componente e compara com a anterior":
it("renderiza Botao corretamente", () => {
const { container } = render(<Botao>Clique</Botao>);
expect(container).toMatchSnapshot();
});
Funciona pra estrutura simples. Não use pra
componentes de UI: cada mudança de classe CSS,
de texto, de ordem de elementos gera snapshot
diff, e o time aprende a "aceitar tudo" com
--updateSnapshot. É degradação de qualidade.
Use snapshot só pra:
- Saída de função pura determinística
(ex:
formatarHTML(input)que retorna string). - Componentes de baixo nível sem estilo (ex: geradores de código).
Matchers customizados com expect.extend.
Pra reusar asserções específicas do domínio:
expect.extend({
toBeValidCPF(received: string) {
const isValid = /^\d{3}\.\d{3}\.\d{3}-\d{2}$/.test(received);
return {
pass: isValid,
message: () => `expected ${received} to be a valid CPF`,
};
},
});
// Uso:
expect("123.456.789-00").toBeValidCPF();
Útil em time grande com regras de negócio repetidas (CPF, CNPJ, telefone, UUID, etc).
Pra quem quer ir além 🔴
Vitest vs Jest em 2026. Jest dominou até 2022. Vitest (lançado em 2021) tomou o espaço em 2024 porque:
- ESM nativo - Jest ainda transpila via
Babel. Vitest usa Vite, que entende ESM
direto. Menos config, menos dor com
module.exportsvsimport. - Velocidade - Vitest é 2-5x mais rápido em suites grandes. Watch mode usa HMR do Vite.
- TypeScript first -
expect()é type-checked,vi.fn<T>()é genérico. Jest tem tipagem parcial e exige@types/jest. - Compatibilidade com Vite - se você usa Vite (Vite + React, TanStack Start, etc.), a config do Vitest é zero.
- API quase idêntica - se você sabe Jest,
sabe Vitest. Migração é
import { vi } from "vitest"em vez dejest.
Hoje a única razão pra ainda usar Jest é projeto legado ou ferramentas que assumem Jest (alguns plugins Storybook antigos, alguns templates CRA).
Snapshot: por que evitar. A comunidade de testing convergence em torno da posição do Kent C. Dodds: snapshot de UI é armadilha. Componentes mudam muito (classe CSS, ordem de elementos, whitespace), e cada diff vira "aceito sem ler" no PR. O time perde o sinal.
Caminho melhor: testar comportamento com
Testing Library (nó 3), e ignorar estrutura
de DOM. Se precisar de visual regression,
usar Playwright toHaveScreenshot (nó 4) -
a baseline visual é explícita e revisada
visualmente.
Mutation testing: o teste do teste. Stryker
(mutation testing framework) é a forma de saber
se seus testes são efetivos. Ele faz mudanças
aleatórias no código (+ vira -, > vira
>=), roda os testes, e conta quantas mutações
sobreviveram (testes não pegaram). Se você tem
100% de coverage mas 50% de mutation score,
seus testes são fracos. Stryker para JS existe
mas tem custo alto de CI (3-5x o tempo normal).
Mencionado como aprofundamento - pertence a
trilha de qualidade avançada.
Mock de fetch com MSW (referência). No nó 3
você vai ver que mockar fetch direto com
vi.spyOn(global, "fetch") é frágil. O caminho
padrão em 2026 é MSW (Mock Service Worker):
um service worker que intercepta requests de
rede e devolve respostas mockadas. Funciona
em Node (test) e em browser (dev). Detalhes
no nó 3, mas se você já usa, é o caminho.
Leitura recomendada:
- Vitest - Getting Started - a doc oficial, 5 minutos de leitura.
- Vitest - API - todas as funções e matchers.
- Autonoma - Jest vs Vitest 2026 - comparativo direto com benchmarks.
Dica: o erro mais comum no começo é mockar demais. Teste a função que você escreveu usando a entrada/saída real. Só mock quando há dependência externa (fetch, Date, localStorage, ID random). Mockar lógica interna "porque é difícil" é sinal de que a função está fazendo coisa demais - refatore em funções menores.
No próximo nó, vamos subir pra Testing Library:
testar componente React pelo que o usuário vê, com
queries por papel ARIA, userEvent, async, e MSW
pra mockar fetch.
// Quiz
Qual a diferença prática entre `vi.fn()` e `vi.mock()` no Vitest?