Alternar tema

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

Easton editorial illustration: failed red test card, passing green test card, central refactor wrench, coverage gate

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.

50 mil testes
Vitest 3 segundos vs Jest 28-34 segundos

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/else foi 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

ErroCausaSolução
Cannot find moduleAlias de caminho não configurado corretamenteVerifique o alias no vitest.config
vi.mock is not a functionForma de importação incorretaUse import { vi } from 'vitest'
Fuso horário incorreto nos testesO padrão é UTCConfigure 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. 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. 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. 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. 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. 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?
O Vitest foi criado para o Vite, tem suporte nativo a ESM e costuma ser 5 a 10 vezes mais rápido que o Jest. Ele também reutiliza automaticamente a configuração do Vite, como aliases e variáveis de ambiente, sem exigir uma configuração extra como moduleNameMapper no Jest.
Como funciona o ciclo Red-Green-Refactor no TDD?
É um ciclo em três passos:

• 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?
A recomendação é algo entre 75% e 85%. Baixo demais não ajuda muito; alto demais vira uma corrida desgastante. Em geral, 80% já cobre a lógica central, enquanto os 20% restantes costumam ser casos extremos que podem consumir tempo demais se forem testados à força.
Como escolher entre vi.fn, vi.spy e vi.mock?
Escolha pelo cenário:

• 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?
Os testes incrementais rodam apenas os arquivos afetados. Em um projeto com 800 testes, uma execução completa pode levar 4 segundos, enquanto a execução incremental costuma levar apenas algumas centenas de milissegundos. Depois de salvar o arquivo, os testes rodam quase na hora, sem trocar de janela.
Como resolver problemas de fuso horário em testes?
Por padrão, o Vitest usa o fuso UTC. Defina process.env.TZ = 'Asia/Shanghai' em setupFiles no vitest.config.ts, ou configure manualmente no início do arquivo de teste.

13 min de leitura · Publicado em: 29 abr 2026 · Atualizado em: 14 jul 2026

Comentários

Entre com GitHub para comentar

Easton BlogEaston Blog