Teste unitário na prática com Vitest
Da teoria pra sintaxe de verdade. Como escrever describe, it e expect com Vitest, como mockar uma dependência externa, e a diferença entre testar comportamento e testar implementação.
Já expliquei aqui os conceitos por trás de teste de software, mas fiquei devendo a parte prática: como isso realmente aparece escrito num arquivo, rodando contra código de verdade. Vou usar o Vitest, que hoje é a escolha mais comum em projeto novo com Vite ou mesmo em projeto Node puro, por ser rápido e ter uma sintaxe praticamente igual ao Jest.
A estrutura básica
Um teste no Vitest é organizado em três blocos: describe agrupa testes relacionados, it (ou test) descreve um caso específico, e expect verifica se o resultado é o esperado.
// calcular-desconto.js
export function calcularDesconto(valor, percentual) {
if (percentual < 0 || percentual > 100) {
throw new Error("percentual inválido");
}
return valor - (valor * percentual) / 100;
}// calcular-desconto.test.js
import { describe, it, expect } from "vitest";
import { calcularDesconto } from "./calcular-desconto";
describe("calcularDesconto", () => {
it("aplica o percentual de desconto corretamente", () => {
expect(calcularDesconto(100, 10)).toBe(90);
});
it("retorna o valor cheio quando o desconto é zero", () => {
expect(calcularDesconto(100, 0)).toBe(100);
});
it("lança erro quando o percentual é inválido", () => {
expect(() => calcularDesconto(100, 150)).toThrow("percentual inválido");
});
});Rodando npx vitest, cada it roda isoladamente, e o terminal mostra exatamente qual passou e qual falhou, com o valor esperado e o valor recebido lado a lado quando alguma expectativa não bate.
Testar comportamento, não implementação
Um erro comum em quem está começando com teste é testar demais o "como" em vez do "o quê". Um teste amarrado à implementação interna quebra toda vez que você refatora o código, mesmo que o resultado final continue correto:
// ruim: depende de detalhe interno que pode mudar sem problema nenhum
it("usa a variável temp internamente", () => {
const spy = vi.spyOn(console, "log");
calcularDesconto(100, 10);
expect(spy).toHaveBeenCalled(); // amarrado a detalhe que não devia importar
});O teste bom verifica o resultado observável da função, a entrada que ela recebe e a saída que ela devolve, sem se importar com a variável interna ou a ordem exata dos passos usados pra chegar lá. Isso permite reescrever o corpo da função inteira, desde que o comportamento externo continue igual, sem quebrar nenhum teste por um motivo que não devia importar.
Mockando uma dependência externa
Testes unitários não deveriam depender de rede, banco de dados ou qualquer serviço externo de verdade, porque isso deixa o teste lento, instável (depende da internet estar funcionando) e caro de rodar repetidamente. A solução é mockar essa dependência, substituindo ela por uma versão falsa controlada pelo próprio teste:
// enviar-boas-vindas.js
export async function enviarBoasVindas(email, servicoEmail) {
if (!email.includes("@")) {
throw new Error("e-mail inválido");
}
await servicoEmail.enviar(email, "Bem-vindo!");
return { enviado: true };
}import { describe, it, expect, vi } from "vitest";
import { enviarBoasVindas } from "./enviar-boas-vindas";
describe("enviarBoasVindas", () => {
it("chama o serviço de e-mail com os parâmetros certos", async () => {
const servicoFalso = { enviar: vi.fn().mockResolvedValue(true) };
await enviarBoasVindas("ana@exemplo.com", servicoFalso);
expect(servicoFalso.enviar).toHaveBeenCalledWith("ana@exemplo.com", "Bem-vindo!");
});
it("não chama o serviço de e-mail se o e-mail for inválido", async () => {
const servicoFalso = { enviar: vi.fn() };
await expect(enviarBoasVindas("invalido", servicoFalso)).rejects.toThrow();
expect(servicoFalso.enviar).not.toHaveBeenCalled();
});
});O vi.fn() cria uma função falsa que registra como foi chamada, sem executar nenhum envio de e-mail de verdade. Isso permite confirmar que a lógica de validação e a chamada pro serviço aconteceram corretamente, sem depender de um provedor de e-mail real respondendo durante o teste.
Setup e teardown
Quando vários testes precisam de uma preparação comum, beforeEach evita repetir esse código em cada it:
describe("carrinho de compras", () => {
let carrinho;
beforeEach(() => {
carrinho = criarCarrinho();
});
it("começa vazio", () => {
expect(carrinho.itens).toHaveLength(0);
});
it("adiciona um item", () => {
carrinho.adicionar({ id: "1", preco: 50 });
expect(carrinho.itens).toHaveLength(1);
});
});Cada it recebe um carrinho novo, criado do zero antes de rodar, garantindo que um teste nunca interfere no resultado do outro por compartilhar estado entre eles.
Fechando
A sintaxe do Vitest não é o ponto difícil, describe, it e expect se aprende em poucos minutos. O que separa um teste útil de um teste que só existe pra aumentar porcentagem de cobertura é focar no comportamento observável da função, mockar o que é externo ao código testado, e manter cada teste independente dos outros. Dominar isso é o que transforma teste em uma ferramenta que realmente avisa quando algo quebrou, em vez de um ritual que todo mundo escreve sem confiar de verdade no resultado.
Direto na sua
caixa de entrada.
Um aviso por e-mail sempre que eu publicar um post novo. Sem spam, sem newsletter chata, só isso.
Leia também
- Artboards no Claude Code: a skill /design e como evitar o AI Slop7 de outubro de 2026 · 5 min
- Busca full-text: como implementar pesquisa de verdade7 de outubro de 2026 · 4 min
- Logging estruturado: por que console.log não basta em produção5 de outubro de 2026 · 4 min
- Gauntlet Loop: como usar múltiplos agentes para melhorar o design no Claude Code4 de outubro de 2026 · 8 min