Vitest na prática: fluxo TDD e configuração de cobertura

Ficar olhando para aquela linha no terminal, Test Suites: 1 failed, 47 passed. Você muda uma linha de código e espera 28 segundos até os testes terminarem. Muda mais uma linha, mais 28 segundos.
Essa talvez seja a fotografia mais fiel de uma migração de Jest para Vitest.
Na época, o projeto tinha quase 500 casos de teste. Cada npm test dava tempo de rolar duas páginas do Hacker News. Depois que trocamos para Vitest, o mesmo conjunto de testes passou a terminar em pouco mais de 3 segundos.
Então hoje quero falar de duas coisas: como usar o Vitest para chegar a essa experiência de testes rápidos e como usar TDD, ou desenvolvimento orientado a testes, para tornar a escrita de testes menos dolorosa. Vou usar uma função completa de formatação de preço como exemplo, passar pelo ciclo Red-Green-Refactor e depois falar sobre configuração de cobertura, técnicas de Mock e o Vitest UI como ferramenta de debug.
Por que escolher Vitest + TDD
Vamos começar com um dado.
A SitePoint fez um comparativo em 2026: 50 mil casos de teste foram executados pelo Vitest em 3 segundos. E o Jest? Entre 28 e 34 segundos. Não é uma pequena diferença; é uma diferença de ordem de grandeza.
Velocidade é só um dos motivos. Se você já usou Jest para lidar com módulos ESM, provavelmente também caiu naquele buraco: instalar babel, configurar transformers e ainda torcer para que todas as strings mágicas em jest.config.js funcionem. O Vitest é diferente. Ele oferece suporte nativo a ESM e não exige nenhuma configuração de transpilação. Seu código é executado nos testes do jeito que você escreveu. Simples e direto.
Há outro ponto que faz ainda mais sentido para quem usa Vite: o Vitest reutiliza diretamente a configuração do Vite. Os aliases, variáveis de ambiente e plugins definidos em vite.config.ts são herdados automaticamente pelo ambiente de teste. Não é preciso escrever outro moduleNameMapper como no Jest. Na primeira vez que percebi isso, fiquei parado alguns segundos: então configuração de teste podia ser simples assim?
Falando da ferramenta, vamos ao método. Muita gente já ouviu falar de TDD, mas pouca gente realmente mantém a prática. A ideia central é um ciclo chamado Red-Green-Refactor: primeiro escreva um teste que falha (vermelho), depois escreva apenas o código suficiente para passar (verde) e, por fim, refatore. Parece contraintuitivo, certo? Escrever teste antes do código?
Mas existe uma vantagem importante aqui: cada linha de código que você escreve existe para fazer um teste passar. Não há lógica sobrando, nem código de “vai que um dia precisa”. Como o teste vem antes, você é obrigado a pensar primeiro no que a função deve fazer, no que deve retornar e onde estão os limites. Essa restrição acaba deixando o design mais claro.
O modo watch do Vitest torna esse ciclo especialmente fluido. Assim que o arquivo é salvo, os testes rodam em segundos e o resultado aparece direto no terminal. Você não precisa trocar de janela nem executar comandos manualmente. É como ter uma revisão constante dizendo: “opa, essa mudança quebrou o teste” e, logo depois, “agora está tudo verde”. Esse feedback imediato ajuda você a entrar em estado de fluxo sem perceber.
TDD na prática: criando uma função do zero
Só falar não resolve. Vamos desenvolver uma função formatPrice() usando TDD para transformar um número em exibição monetária. Por exemplo, transformar 1234.5 em ¥1,234.50.
Fase Red: primeiro escreva um teste que falha
Abra o projeto e crie um arquivo formatPrice.test.ts:
// formatPrice.test.ts
import { describe, it, expect } from 'vitest'
import { formatPrice } from './formatPrice'
describe('formatPrice', () => {
it('deve formatar o número como moeda chinesa', () => {
expect(formatPrice(1234.5)).toBe('¥1,234.50')
})
})
Agora execute npx vitest. O terminal vai mostrar um grande erro em vermelho: Cannot find module './formatPrice'. Isso acontece porque a função ainda nem existe.
E está tudo certo. Essa é a fase Red: o teste falha e mostra que você definiu um requisito ainda não implementado. Muita gente acha estranho escrever o teste primeiro, mas pense bem: se você escreve o código antes e só depois escreve o teste, como sabe que o teste realmente verifica aquilo que você queria verificar?
Fase Green: escreva o menor código possível para passar
Agora crie formatPrice.ts com o mínimo necessário para o teste passar:
// formatPrice.ts
export function formatPrice(value: number): string {
return '¥1,234.50' // primeiro, devolvemos um valor fixo
}
Execute os testes de novo. Verde!
Espere, talvez você diga: “mas isso é trapaça”. Na verdade, não. O TDD enfatiza escrever o código que “basta” para passar no teste, nem mais nem menos. Pode ser um valor fixo ou a lógica mais simples possível. Desde que o teste passe, você tem uma base verificável. Depois você adiciona outro teste, ajusta o código e avança passo a passo.
Agora adicione outro caso de teste:
it('deve lidar corretamente com valores diferentes', () => {
expect(formatPrice(0)).toBe('¥0.00')
expect(formatPrice(99.99)).toBe('¥99.99')
})
O teste fica vermelho de novo. Agora não dá mais para manter o valor fixo; é hora de escrever a lógica real:
export function formatPrice(value: number): string {
return `¥${value.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',')}`
}
Rode os testes. Tudo verde. Essa expressão regular é um pouco feia, mas, por enquanto, funciona.
Fase Refactor: melhore a estrutura do código
Os testes passaram, mas o código ainda pode ficar mais claro. Agora dá para refatorar com tranquilidade, porque os testes estão protegendo você: se algo quebrar, o vermelho aparece na hora.
// versão depois da refatoração
export function formatPrice(value: number): string {
// Intl.NumberFormat deixa a formatação mais robusta
return new Intl.NumberFormat('zh-CN', {
style: 'currency',
currency: 'CNY',
minimumFractionDigits: 2,
}).format(value)
}
Execute os testes mais uma vez. Continuam verdes. Refatoração concluída.
Percebe? Essa é uma volta completa do Red-Green-Refactor. Você começa com uma falha, escreve o código mais simples e depois melhora a estrutura. Durante todo o processo, os testes ficam como rede de proteção. Como cada passo é pequeno, a carga mental também diminui: você não precisa resolver todos os casos de borda de uma vez, porque os testes vão lembrando você.
Em projetos reais, costumo rodar esse ciclo no modo watch. Salvar arquivo -> teste roda automaticamente -> ver resultado -> mudar código -> salvar -> rodar de novo. Tudo leva poucos segundos e você nem precisa sair do editor. Aquela sensação de “mudei algo e já sei se está certo” é realmente muito boa.
Configuração de cobertura e integração com CI
Depois de escrever testes, você precisa saber quanto do código foi exercitado. É para isso que serve o relatório de cobertura.
Configuração básica
Adicione a configuração de cobertura em vitest.config.ts:
// vitest.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
coverage: {
provider: 'v8', // ou 'istanbul'; v8 costuma ser mais rápido por padrão
reporter: ['text', 'html', 'json-summary'],
reportsDirectory: './coverage',
include: ['src/**/*.ts'],
exclude: ['src/**/*.test.ts', 'src/types/**'],
thresholds: {
statements: 80,
branches: 75,
functions: 80,
lines: 80,
},
},
},
})
O provider tem duas opções: v8 e istanbul. O v8 usa a API nativa de cobertura do motor V8 e é mais rápido. O istanbul é uma solução mais antiga e costuma ter melhor compatibilidade. Se o seu projeto é um ambiente Vite/Node mais puro, v8 normalmente basta.
O reporter define o formato de saída: text imprime no terminal, html gera um relatório visual e json-summary é útil para ferramentas de CI.
Definindo limites
A parte de thresholds merece uma explicação. Ela tem quatro dimensões:
- statements: cobertura de instruções, ou quantas linhas de código foram executadas
- branches: cobertura de ramificações, ou se cada ramo de
if/elsefoi testado - functions: cobertura de funções, ou quantas funções foram chamadas
- lines: cobertura de linhas, parecida com statements, mas calculada de um jeito um pouco diferente
Eu costumo configurar thresholds entre 75% e 85%. Baixo demais não ajuda muito; alto demais deixa a equipe correndo atrás de número. Alguns trechos, como validações de borda e tratamento de erro, podem ser realmente difíceis de levar a 100%.
Integração com GitHub Actions
O lugar onde a cobertura ganha mais valor é no CI, bloqueando automaticamente PRs abaixo do limite. Em .github/workflows/test.yml, adicione:
- name: Run tests with coverage
run: npm run test -- --coverage
- name: Check coverage threshold
run: |
COVERAGE=$(cat coverage/coverage-summary.json | jq '.total.lines.pct')
if (( $(echo "$COVERAGE < 80" | bc -l) )); then
echo "Coverage $COVERAGE% is below threshold 80%"
exit 1
fi
Assim, se a cobertura ficar abaixo de 80%, o PR não poderá ser mesclado. A equipe precisa garantir que os testes foram escritos antes de enviar a mudança.
Como ler o relatório
Depois de executar npx vitest --coverage, o terminal mostra um resultado parecido com este:
% Stmts % Branch % Funcs % Lines Uncovered Line
----------|----------|----------|----------|----------------
82.45 | 76.32 | 85.71 | 82.45 | 23-25, 67
A coluna Uncovered Line lista as linhas que não foram cobertas pelos testes. Ao abrir coverage/index.html, você vê um relatório visual mais detalhado: verde para código testado, vermelho para código não testado.
Para ser sincero, quando comecei a perseguir cobertura, também fiquei meio obcecado e queria testar cada linha. Depois percebi que não precisava. Uma cobertura de 80% geralmente já inclui a lógica central e os principais ramos. Os 20% restantes muitas vezes são casos extremos, e testá-los à força pode desperdiçar tempo.
Os três mosqueteiros do Mock: vi.fn, vi.spy, vi.mock
A parte mais trabalhosa dos testes costuma ser lidar com dependências externas: chamadas de API, timers, bibliotecas de terceiros. O Vitest oferece três formas de Mock, cada uma com seu uso.
vi.fn(): criar uma função falsa
Quando você precisa de uma função “falsa” e não se importa com o que ela era originalmente, apenas com como ela foi chamada, use vi.fn().
test('a função de callback deve ser chamada uma vez', () => {
const callback = vi.fn()
callMeMaybe(callback)
expect(callback).toHaveBeenCalledTimes(1)
expect(callback).toHaveBeenCalledWith('hello')
})
Aqui, callback é uma função totalmente nova. Ela registra quantas vezes foi chamada, com quais parâmetros e quais valores retornou. Você pode usar mockReturnValue para definir o retorno e mockImplementation para definir o comportamento.
vi.spy(): observar uma função real
Às vezes, você não quer substituir a função. Só quer ver se ela foi chamada e quais parâmetros recebeu. Nesse caso, use vi.spyOn.
test('deve chamar console.log', () => {
const logSpy = vi.spyOn(console, 'log')
greet('World')
expect(logSpy).toHaveBeenCalledWith('Hello, World!')
logSpy.mockRestore() // não esqueça de restaurar
})
O spy preserva o comportamento original da função e apenas adiciona a observação. Depois de usar, lembre-se de chamar mockRestore(), senão outros testes podem ser afetados.
vi.mock(): substituir um módulo inteiro
Quando você precisa simular uma resposta de API ou substituir uma biblioteca de terceiros, use vi.mock. Ele troca o módulo inteiro.
// simulando axios
vi.mock('axios', () => ({
default: {
get: vi.fn(() => Promise.resolve({ data: { name: 'test' } }))
}
}))
test('getUser deve retornar dados do usuário', async () => {
const user = await getUser(1)
expect(user.name).toBe('test')
expect(axios.get).toHaveBeenCalledWith('/users/1')
})
Há uma pegadinha em vi.mock: ele é içado para o topo do arquivo, não importa se você o escreveu dentro de uma função ou de um if. Por isso, o conteúdo do mock não deve depender de outras variáveis locais.
Qual deles escolher?
De forma simples:
- Precisa apenas de uma função falsa? Use
vi.fn() - Quer observar uma função real? Use
vi.spy() - Precisa substituir um módulo inteiro? Use
vi.mock()
Eu também confundia os três com frequência. Depois encontrei um jeito de lembrar: fn fabrica, spy observa, mock troca tudo. Não é bonito, mas funciona.
Limpar é importante
Testes não devem interferir uns nos outros. Essa é a base da confiabilidade. Depois de cada teste, lembre-se de limpar os mocks:
afterEach(() => {
vi.restoreAllMocks()
})
Ou ative isso globalmente na configuração do Vitest:
test: {
restoreMocks: true
}
Vitest UI e técnicas de debug
Ver o resultado dos testes pela linha de comando já é suficiente, mas, se você quiser uma experiência mais visual, vale testar o Vitest UI.
Iniciar a interface visual
npx vitest --ui
O navegador abre automaticamente uma interface: à esquerda fica a lista de testes, à direita os detalhes. Ao clicar em qualquer teste, você vê a saída completa, a stack trace do erro e o tempo de execução. Também há um botão de cobertura que abre o relatório HTML mencionado antes.
Essa ferramenta é especialmente útil para debug. Quando um teste falha, você não precisa ficar procurando no terminal. Dá para ver a mensagem de erro direto na UI, com o código ao lado; depois de corrigir e salvar, a interface atualiza automaticamente.
Modo watch: executar apenas os testes afetados
No desenvolvimento do dia a dia, costumo rodar npx vitest em modo watch. O teste incremental é esperto: se você mudou utils/formatPrice.ts, ele executa apenas os testes relacionados a esse arquivo, sem rodar tudo de novo.
Quando a suíte cresce, essa reexecução seletiva economiza muito tempo. Em um projeto meu com mais de 800 testes, a execução completa levava 4 segundos; a incremental normalmente levava apenas algumas centenas de milissegundos.
Técnicas de debug
O que fazer quando o teste falha? Algumas técnicas comuns:
Rodar apenas um teste: adicione .only depois de it
it.only('este teste tem problema, rode isolado', () => {
// ...
})
Pular um teste: adicione .skip
it.skip('pule este por enquanto', () => {
// ...
})
Atualizar snapshots: quando a estrutura do componente mudou e o snapshot falhou
npx vitest -u # -u é a abreviação de --update
Debug com console.log: sim, o método antigo continua sendo útil. O Vitest mostra a saída completa do console nos resultados do teste.
Erros comuns
| Erro | Causa | Solução |
|---|---|---|
Cannot find module | Alias de caminho não configurado corretamente | Verifique o alias no vitest.config |
vi.mock is not a function | Forma de importação incorreta | Use import { vi } from 'vitest' |
| Fuso horário incorreto nos testes | O padrão é UTC | Configure process.env.TZ = 'Asia/Shanghai' no setup |
Já passei por todos esses problemas. O de fuso horário, especialmente, é clássico: no CI, todos os testes com hora local falhavam, e só depois de muita investigação percebi que era o fuso.
Conclusão
Depois de tudo isso, a ideia central é simples: Vitest + TDD pode tornar a escrita de testes muito menos dolorosa.
Na velocidade, sair de dezenas de segundos no Jest para poucos segundos muda mais do que um número; muda a experiência. Você não fica alternando entre editar código e esperar teste, nem sofre com aquela sensação de “mudei uma linha e vou esperar uma eternidade”. Suporte nativo a ESM e reutilização da configuração do Vite também poupam bastante atrito.
O ciclo Red-Green-Refactor do TDD parece um pouco contraintuitivo, mas, depois de tentar algumas vezes, você começa a perceber a vantagem: cada passo é pequeno, cada passo tem verificação, e a carga mental fica leve. Você não precisa desenhar uma solução perfeita de uma vez; os testes ajudam a encontrar problemas e iterar.
Configuração de cobertura e técnicas de Mock são ferramentas. Dominar essas peças ajuda você a escrever testes mais robustos. Mas o mais importante é criar o hábito de testar, não para perseguir números, e sim para ter mais confiança no código.
Se o seu projeto já usa Vite, migrar de Jest para Vitest costuma ter custo baixo. Adicione npm add -D vitest, ajuste a sintaxe dos testes de Jest para Vitest, que é quase igual, e a suíte já consegue rodar. Se ainda estiver em dúvida, teste primeiro em um módulo pequeno e sinta o feedback imediato do modo watch.
Agora rode seu primeiro teste com Vitest e experimente o ciclo TDD. Sinta aquela tranquilidade de mudar algo e saber quase imediatamente se continua correto. Talvez você também acabe, como eu, gostando de escrever testes.
Fluxo prático de TDD com Vitest
Desenvolva uma função do zero usando TDD e configure o relatório de cobertura
⏱️ Estimated time: 30 min
- 1
Step 1: Instalar o Vitest
Adicione o Vitest a um projeto Vite:
npm add -D vitest
Não é preciso configuração extra: o Vitest reutiliza automaticamente a configuração do Vite - 2
Step 2: Fase Red: escrever um teste que falha
Crie o arquivo de teste e escreva um teste que certamente vai falhar:
import { describe, it, expect } from 'vitest'
import { formatPrice } from './formatPrice'
describe('formatPrice', () => {
it('deve formatar moeda', () => {
expect(formatPrice(1234.5)).toBe('¥1,234.50')
})
})
Execute npx vitest e confirme que o teste falhou (vermelho) - 3
Step 3: Fase Green: escrever o menor código possível
Crie o arquivo de implementação e escreva apenas o código suficiente para passar no teste:
export function formatPrice(value: number): string {
return '¥1,234.50' // primeiro, um valor fixo
}
Execute os testes e confirme que passaram (verde) - 4
Step 4: Fase Refactor: melhorar o código
Refatore usando Intl.NumberFormat:
export function formatPrice(value: number): string {
return new Intl.NumberFormat('zh-CN', {
style: 'currency',
currency: 'CNY',
}).format(value)
}
Confirme que os testes continuam passando - 5
Step 5: Configurar cobertura
Adicione ao vitest.config.ts:
test: {
coverage: {
provider: 'v8',
thresholds: { statements: 80, branches: 75 }
}
}
Execute npx vitest --coverage para ver o relatório
FAQ
Qual é a principal diferença entre Vitest e Jest?
Como funciona o ciclo Red-Green-Refactor no TDD?
• Red: primeiro escreva um teste que falha para definir o requisito
• Green: escreva o menor código possível para o teste passar, até com valor fixo se fizer sentido
• Refactor: melhore a estrutura do código protegido pelos testes
Cada passo é pequeno, a carga mental fica menor e os testes acompanham tudo.
Qual limite de cobertura faz sentido configurar?
Como escolher entre vi.fn, vi.spy e vi.mock?
• vi.fn(): cria uma função falsa do zero e registra chamadas e parâmetros
• vi.spy(): observa uma função existente, preserva o comportamento original e exige mockRestore() ao final
• vi.mock(): substitui um módulo inteiro, útil para simular APIs ou bibliotecas de terceiros
Um jeito de lembrar: fn fabrica, spy observa, mock troca tudo.
Qual é a vantagem do modo watch do Vitest?
Como resolver problemas de fuso horário em testes?
13 min de leitura · Publicado em: 29 abr 2026 · Atualizado em: 14 jul 2026
Guia de testes Vitest
Se você chegou pela busca, o caminho mais rápido é ir para o post anterior ou próximo desta série.
Anterior
Testes unitários com Vitest: da configuração ao fluxo de TDD
Aprenda a configurar testes unitários com Vitest, aplicar TDD, criar mocks e definir cobertura. Veja também como migrar do Jest com poucas mudanças.
Parte 1 de 3
Próximo
Testes de componentes com Vitest: Browser Mode e integração com Playwright
Guia prático do Vitest Browser Mode: configure a integração com Playwright, teste componentes React/Vue, implemente gates de cobertura no CI e compare jsdom com testes em navegador real.
Parte 3 de 3



Comentários
Entre com GitHub para comentar