Testes unitários com Vitest: da configuração ao fluxo de TDD

Quanto tempo leva para configurar o Jest em um projeto ESM? É preciso lidar com ts-jest, babel-jest, jest.config.js… e ainda resolver vários problemas de módulos. Certa vez, passei uma tarde inteira só para fazer o Jest reconhecer corretamente uma instrução import de um arquivo .vue.
Com o Vitest, bastou uma linha de configuração.
Não é exagero. Quando executei vitest pela primeira vez em um projeto Vite e vi os testes terminarem em poucos segundos, a sensação foi a de trocar um computador que travava havia três anos por um novo: tudo ficou mais leve.
Até que ponto o Vitest é rápido? Dados oficiais e testes da comunidade indicam uma inicialização a frio de cerca de 200 ms, contra 2 a 4 segundos do Jest. Cerca de 500 casos de teste terminam em 8 segundos, enquanto o Jest leva por volta de 45 segundos. Melhor ainda: ele compartilha a configuração com o Vite, oferece suporte nativo a TypeScript e tem uma API quase idêntica à do Jest. A migração pode levar apenas meia hora.
Aqui você vai da configuração inicial a um fluxo completo de TDD, incluindo técnicas de mocking e ajustes de cobertura. Seja para adotar o Vitest em um projeto novo ou migrar um projeto antigo que usa Jest, as etapas necessárias estão reunidas a seguir.
O que é o Vitest e por que ele é tão rápido?
Em poucas palavras, o Vitest é um framework de testes nativo do Vite.
Se você já usa o Vite para compilar o projeto, o Vitest praticamente funciona sem configuração adicional. Ele reutiliza diretamente as opções do Vite: aliases, variáveis de ambiente e processamento de CSS são herdados automaticamente. Não é preciso recriar no Jest toda aquela configuração de transform, moduleFileExtensions e moduleNameMapper.
Há três vantagens principais:
Velocidade. A inicialização a frio leva cerca de 200 milissegundos, enquanto o Jest normalmente precisa de 2 a 4 segundos. A diferença fica ainda mais evidente em projetos grandes: para 500 casos de teste, o Vitest leva aproximadamente 8 segundos e o Jest, cerca de 45 segundos, segundo uma medição publicada pela DEV Community em 2026. Não é uma diferença pequena.
Compatibilidade com o Jest. A API é praticamente a mesma: describe, it, expect e vi.fn() seguem uma forma de uso familiar para quem já conhece o Jest. Para migrar, em muitos casos basta alterar os caminhos de importação.
Modo de observação inteligente. O recurso chamado “HMR for tests” executa novamente apenas os testes relacionados ao código alterado, em vez de repetir toda a suíte. Durante o desenvolvimento, esse feedback imediato faz diferença.
Agora que está claro o que ele é, vamos à instalação.
Instalação e configuração
Primeiro, os requisitos do sistema: Vite 6.0.0 ou superior e Node 20.0.0 ou superior. Em projetos recentes, essas versões provavelmente já estão em uso.
Instalação
Um único comando:
npm install -D vitest
É só isso. Não é necessário instalar vários pacotes como @types/jest, ts-jest ou jest-environment-jsdom. O Vitest oferece suporte nativo a TypeScript.
Arquivo de configuração
Há duas opções: adicionar o campo test ao vite.config.ts ou criar um arquivo vitest.config.ts separado.
Para um projeto simples, use o próprio vite.config.ts:
// vite.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
globals: true, // Variáveis globais; não é preciso importar describe, it e expect sempre
environment: 'node', // Ou 'jsdom' para testar em um ambiente de navegador
include: ['tests/**/*.test.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'html', 'lcov'],
},
},
})
A opção globals: true é bastante útil: depois de ativá-la, describe, it e expect se tornam variáveis globais, sem a necessidade de importá-las em todos os arquivos de teste. É o mesmo comportamento do Jest.
Se os testes dependem da API do DOM, como em um teste de renderização de componente, troque environment por 'jsdom' e instale o pacote jsdom:
npm install -D jsdom
Scripts de execução
Adicione duas linhas ao package.json:
{
"scripts": {
"test": "vitest",
"test:run": "vitest run"
}
}
npm test ativa o modo de observação e executa novamente os testes quando o código muda. npm run test:run faz uma única execução e encerra, por isso é a opção indicada para CI.
A configuração termina aqui. Comparado à quantidade de opções como preset, transform e moduleFileExtensions no Jest, o processo é bem mais direto.
Como escrever testes unitários
Arquivos de teste costumam usar a extensão .test.ts ou .spec.ts. Eles podem ficar no diretório tests/ ou ao lado do arquivo de origem, conforme a convenção da equipe.
Estrutura básica
Este é um teste mínimo:
import { describe, it, expect } from 'vitest'
import { add, divide } from './math'
describe('Math utilities', () => {
it('should add two numbers', () => {
expect(add(2, 3)).toBe(5)
})
it('should throw on division by zero', () => {
expect(() => divide(10, 0)).toThrow('Division by zero')
})
})
describe agrupa testes, it define um caso de teste e expect cria uma asserção.
Se globals: true estiver ativo, a linha de importação pode ser removida.
Asserções mais usadas
Alguns exemplos frequentes:
// Igualdade básica
expect(value).toBe(5) // Igualdade estrita (===)
expect(obj).toEqual({ a: 1 }) // Igualdade profunda
// Verificação de valores truthy e falsy
expect(value).toBeTruthy()
expect(value).toBeFalsy()
expect(value).toBeNull()
// Exceções
expect(() => fn()).toThrow()
expect(() => fn()).toThrow('Error message')
// Comparações numéricas
expect(n).toBeGreaterThan(10)
expect(n).toBeLessThanOrEqual(5)
// Elemento em array ou padrão em string
expect(arr).toContain('item')
expect(str).toMatch(/pattern/)
Filtragem de testes
Quer executar apenas um teste durante o desenvolvimento? Use .only:
it.only('executa apenas este teste', () => { ... })
Precisa pular um teste temporariamente? Use .skip:
it.skip('não executar temporariamente', () => { ... })
Os dois modificadores também funcionam com describe: describe.only(...) e describe.skip(...).
Execute os testes e observe o resultado. No modo de observação, uma alteração no código mostra imediatamente se o teste ficou verde ou vermelho. O feedback é bem mais rápido do que esperar alguns segundos pela inicialização do Jest a cada vez.
Fluxo prático de desenvolvimento com TDD
O princípio do TDD, ou desenvolvimento orientado a testes, é simples: escreva o teste antes do código.
Pode parecer contraintuitivo, mas a prática obriga você a definir o comportamento esperado da função antes de implementá-la. Quando o teste só é escrito depois do código, ele muitas vezes vira uma formalidade criada apenas para aumentar a cobertura.
O exemplo a seguir percorre o fluxo completo. Vamos implementar uma função de validação de e-mail chamada validateEmail.
Etapa 1: escreva o teste antes de implementar o código
Primeiro, crie o arquivo de teste:
// tests/validateEmail.test.ts
import { describe, it, expect } from 'vitest'
import { validateEmail } from '../src/validateEmail'
describe('validateEmail', () => {
it('should return true for valid email', () => {
expect(validateEmail('[email protected]')).toBe(true)
})
it('should return false for invalid email', () => {
expect(validateEmail('invalid')).toBe(false)
})
})
Nesse momento, a função validateEmail ainda não existe, então o teste necessariamente retorna um erro. Isso é esperado: a primeira etapa do TDD consiste em fazer o teste falhar.
Etapa 2: implemente o mínimo necessário
Agora crie a função com a implementação mais simples que faça o teste passar:
// src/validateEmail.ts
export function validateEmail(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
}
Execute npm test.
Os dois testes ficaram verdes? Ótimo, a etapa 2 está concluída.
Etapa 3: adicione mais casos-limite
Os casos básicos passaram, mas a validação de e-mail tem várias situações de borda. Adicione algumas:
// Acrescente ao arquivo tests/validateEmail.test.ts
it('should return false for empty string', () => {
expect(validateEmail('')).toBe(false)
})
it('should return false for email without domain', () => {
expect(validateEmail('user@')).toBe(false)
})
it('should return false for email with spaces', () => {
expect(validateEmail('test @example.com')).toBe(false)
})
Execute os testes. Se todos passarem, a expressão regular cobre esses casos. Se algum falhar, ajuste a expressão.
Etapa 4: refatore
Com todos os testes passando, você pode refatorar o código com segurança. Por exemplo, pode tornar a expressão regular mais rigorosa ou adicionar um comentário:
// src/validateEmail.ts
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
export function validateEmail(email: string): boolean {
if (!email || email.trim() === '') {
return false
}
return EMAIL_REGEX.test(email)
}
Depois da alteração, execute os testes novamente. Se continuarem verdes, a refatoração preservou o comportamento. Essa é a vantagem do TDD: os testes funcionam como uma rede de proteção e mostram imediatamente quando uma mudança introduz um erro.
Por que o TDD funciona bem?
Para ser sincero, eu também estranhei a ideia de escrever o teste primeiro. Depois de praticar algumas vezes, percebi três benefícios:
- Pensar antes de implementar: escrever o teste significa projetar o comportamento da função e força você a esclarecer entradas e saídas.
- Iterar rapidamente: no modo de observação do Vitest, cada alteração produz feedback em segundos.
- Refatorar com segurança: com testes cobrindo o comportamento, fica mais fácil alterar o código sem receio.
Comece por funções simples, como utilitários e formatadores. Depois de se acostumar, avance para lógicas mais complexas.
Técnicas avançadas de mocking
Em testes unitários, é comum precisar simular dependências externas, como requisições de API, consultas ao banco de dados ou bibliotecas de terceiros. É aí que entram os mocks.
O Vitest oferece uma API de mocks semelhante à do Jest, centrada no objeto vi.
vi.fn(): simule uma única função
A forma mais simples cria uma função simulada:
import { vi, describe, it, expect } from 'vitest'
describe('vi.fn() demo', () => {
it('tracks calls', () => {
const mockFn = vi.fn()
mockFn('hello')
mockFn('world')
expect(mockFn).toHaveBeenCalledTimes(2)
expect(mockFn).toHaveBeenNthCalledWith(1, 'hello')
})
})
Também é possível definir antecipadamente um valor de retorno:
const mockFn = vi.fn().mockReturnValue('mocked result')
// Ou um retorno assíncrono
const asyncMock = vi.fn().mockResolvedValue({ data: 'ok' })
vi.mock(): simule um módulo inteiro
Precisa testar uma função que depende de uma API externa? Simule diretamente o módulo:
import { vi, describe, it, expect, beforeEach } from 'vitest'
import { fetchUser } from './api'
import { UserService } from './UserService'
// Simula o módulo api
vi.mock('./api', () => ({
fetchUser: vi.fn().mockResolvedValue({ id: 1, name: 'Alice' })
}))
describe('UserService', () => {
beforeEach(() => {
vi.clearAllMocks() // Limpa o histórico de chamadas antes de cada teste
})
it('should fetch user', async () => {
const service = new UserService()
const user = await service.getUser(1)
expect(user.name).toBe('Alice')
expect(fetchUser).toHaveBeenCalledWith(1)
})
})
vi.mock() é executado antes da importação do módulo, por isso deve ficar no início do arquivo.
vi.spyOn(): monitore uma função real
Às vezes, você não quer substituir a função inteira, apenas observar suas chamadas:
import { vi, describe, it, expect, afterEach } from 'vitest'
import { calculator } from './calculator'
describe('spyOn demo', () => {
afterEach(() => {
vi.restoreAllMocks()
})
it('tracks add calls', () => {
const addSpy = vi.spyOn(calculator, 'add')
const result = calculator.add(2, 3)
expect(result).toBe(5) // A função original é executada normalmente
expect(addSpy).toHaveBeenCalledWith(2, 3) // A chamada também é registrada
})
})
spyOn é menos invasivo que mock: a função continua funcionando e apenas recebe um monitor.
Simulação de objetos globais
Não quer fazer uma requisição HTTP real durante o teste? Simule o fetch global:
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({ data: 'mocked' })
}))
Com vi.stubGlobal, também é possível simular outros objetos globais do navegador, como window e localStorage.
Mocking costuma ser a parte mais complicada dos testes. Comece com o uso simples de vi.fn() e passe para vi.mock() quando estiver confortável. Não se esqueça de limpar o estado dos mocks depois de cada teste com clearAllMocks ou restoreAllMocks; caso contrário, um teste pode contaminar o outro.
Cobertura e boas práticas
A cobertura de testes é uma métrica de referência para a qualidade do código. O Vitest oferece dois providers de cobertura: v8, que é mais rápido e tem suporte nativo, e istanbul, que oferece compatibilidade mais ampla. Em geral, v8 é suficiente.
Configuração da cobertura
Adicione ao vite.config.ts:
test: {
coverage: {
provider: 'v8',
reporter: ['text', 'html', 'lcov'], // Formatos de saída
thresholds: {
lines: 80, // Limite de cobertura de linhas
functions: 80, // Limite de cobertura de funções
branches: 70, // Limite de cobertura de ramificações
},
exclude: ['node_modules/', 'tests/', '**/*.d.ts'],
},
}
Execute o comando:
vitest run --coverage
O terminal exibe o relatório de cobertura e também cria o diretório coverage/. Nele, há um relatório HTML que mostra quais trechos de código ainda não estão cobertos.
Por que definir limites mínimos?
thresholds não é apenas decorativo. Se a cobertura ficar abaixo dos valores definidos, o Vitest retorna um erro e encerra a execução. Isso é útil em CI porque exige um nível mínimo de testes e impede que código insuficientemente testado seja aceito sem aviso.
Ainda assim, não defina limites altos demais. Uma cobertura de 80% é um ponto de partida razoável; exigir 100% pode consumir tempo da equipe sem melhorar proporcionalmente a qualidade.
Integração com CI/CD
Em GitHub Actions ou outro serviço de CI, adicione uma etapa:
# .github/workflows/test.yml
- name: Run tests with coverage
run: npm run test:run -- --coverage
Depois da execução, você pode enviar o relatório lcov ao Codecov ou Coveralls para acompanhar visualmente a evolução da cobertura.
Recomendações práticas
- Não busque 100%: um número de cobertura atraente não garante código de qualidade. Priorize a lógica essencial e aceite deixar alguns casos periféricos de fora.
- Teste primeiro os fluxos principais: o caminho principal tem prioridade; os ramos de erro vêm depois.
- Remova testes sem utilidade periodicamente: testes também exigem manutenção, então exclua os que ficaram obsoletos ou redundantes.
- Mantenha o modo de observação ativo: deixe o
vitestrodando durante o desenvolvimento para detectar problemas imediatamente.
Conclusão
As principais vantagens do Vitest podem ser resumidas em três palavras: rápido, simples e prático.
Rápido: a inicialização a frio leva 200 ms, e 500 casos de teste terminam em cerca de 8 segundos, quase uma ordem de grandeza mais rápido que o Jest. Simples: ele compartilha a configuração com o Vite e funciona logo após a instalação, sem exigir vários ajustes de transform e moduleNameMapper. Prático: a API é praticamente a mesma do Jest, o que mantém baixo o custo de migração.
Se você usa Vite, o Vitest é a escolha mais direta. Não há motivo para continuar lidando com os problemas de configuração de ESM do Jest.
Se o projeto ainda usa Jest, vale reservar meia hora para testar a migração. Instale o Vitest, altere as importações nos arquivos de teste e, na maioria dos casos, tudo já poderá ser executado.
Quanto ao TDD, não complique. Comece com uma função utilitária simples e escreva o teste antes do código. Depois que isso vira hábito, pensar antes de implementar tende a tornar o trabalho mais eficiente.
Testes não são um peso, mas uma garantia. Dedicar algum tempo à configuração do Vitest deixa o desenvolvimento posterior mais seguro.
Configuração de testes unitários com Vitest e fluxo de TDD
Etapas completas para configurar o Vitest do zero e praticar um fluxo de desenvolvimento orientado a testes
⏱️ Estimated time: 30 min
- 1
Step 1: Instalar o Vitest
Execute o comando de instalação:
```bash
npm install -D vitest
```
Requisitos do sistema: Vite >= 6.0.0 e Node >= 20.0.0 - 2
Step 2: Configurar o vite.config.ts
Adicione o campo test ao arquivo de configuração:
```typescript
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
globals: true,
environment: 'node',
include: ['tests/**/*.test.ts'],
},
})
```
Com globals: true, não é preciso importar describe, it e expect em cada arquivo. - 3
Step 3: Adicionar scripts de execução
Adicione ao package.json:
```json
{
"scripts": {
"test": "vitest",
"test:run": "vitest run"
}
}
```
npm test ativa o modo de observação; npm run test:run executa os testes uma vez, como em um ambiente de CI. - 4
Step 4: Escrever o primeiro teste
Crie o arquivo de teste tests/math.test.ts:
```typescript
import { describe, it, expect } from 'vitest'
describe('Math', () => {
it('should add numbers', () => {
expect(1 + 1).toBe(2)
})
})
```
Execute npm test para confirmar que a configuração funciona. - 5
Step 5: Praticar o fluxo de TDD
Siga o fluxo de desenvolvimento orientado a testes:
• Etapa 1: escreva primeiro o teste para definir o comportamento esperado da função
• Etapa 2: implemente o mínimo de código necessário para o teste passar
• Etapa 3: adicione testes de casos-limite
• Etapa 4: refatore e melhore o código
Use o modo de observação do Vitest para receber feedback em segundos. - 6
Step 6: Configurar a cobertura
Adicione a configuração de cobertura:
```typescript
coverage: {
provider: 'v8',
reporter: ['text', 'html'],
thresholds: {
lines: 80,
functions: 80,
},
}
```
Execute vitest run --coverage para gerar o relatório.
FAQ
Qual é a diferença entre Vitest e Jest?
Como migrar do Jest para o Vitest?
• remova os pacotes relacionados ao Jest e instale vitest
• migre as opções de jest.config.js para vite.config.ts
• nos arquivos de teste, troque import { describe, it, expect } from 'jest' por from 'vitest'
• substitua jest.fn() e jest.mock() por vi.fn() e vi.mock()
Na maioria dos casos, a migração pode ser concluída em 30 minutos.
Quais ambientes de teste o Vitest oferece?
Como simular requisições de API no Vitest?
• vi.fn(): simula uma única função e permite definir o valor de retorno
• vi.mock(): simula um módulo inteiro e substitui todas as exportações
• vi.spyOn(): monitora chamadas de uma função real sem substituir sua implementação
Depois de cada teste, limpe o estado com vi.clearAllMocks() ou vi.restoreAllMocks().
Como configurar a cobertura no Vitest?
Qual é o princípio central do fluxo de TDD?
• vermelho: escreva primeiro um teste que falha
• verde: escreva o mínimo de código necessário para o teste passar
• refatorar: melhore a estrutura do código
Com o modo de observação do Vitest, cada alteração produz feedback em segundos e facilita iterações rápidas. Escrever o teste primeiro também ajuda a esclarecer o design da função.
13 min de leitura · Publicado em: 14 abr 2026 · Atualizado em: 4 set 2026
Guia de testes Vitest
Você está lendo o primeiro post desta série. Continue para o próximo ou abra o hub da série para ver toda a trilha.
Anterior
Você está no início desta série.
Próximo
Este é o post mais recente da série até agora.



Comentários
Entre com GitHub para comentar