Alternar tema

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

Easton editorial illustration: one large test card moving through a three-stage TDD loop

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:

  1. Pensar antes de implementar: escrever o teste significa projetar o comportamento da função e força você a esclarecer entradas e saídas.
  2. Iterar rapidamente: no modo de observação do Vitest, cada alteração produz feedback em segundos.
  3. 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

  1. 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.
  2. Teste primeiro os fluxos principais: o caminho principal tem prioridade; os ramos de erro vêm depois.
  3. Remova testes sem utilidade periodicamente: testes também exigem manutenção, então exclua os que ficaram obsoletos ou redundantes.
  4. Mantenha o modo de observação ativo: deixe o vitest rodando 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. 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. 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. 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. 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. 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. 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?
O Vitest é um framework de testes nativo do Vite e compartilha a configuração com ele. Sua inicialização a frio leva cerca de 200 ms, enquanto o Jest leva de 2 a 4 segundos. A API é quase totalmente compatível com a do Jest, o que reduz bastante o custo de migração. As principais vantagens são velocidade, configuração simples e suporte nativo a TypeScript.
Como migrar do Jest para o Vitest?
As etapas são simples:

• 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?
O Vitest oferece vários ambientes: node, o padrão para testes de backend; jsdom, que simula o DOM do navegador; e happy-dom, uma alternativa de DOM mais rápida. Defina o ambiente com o campo environment. Para testar componentes que dependem do navegador, também é preciso instalar o pacote jsdom.
Como simular requisições de API no Vitest?
Há três formas comuns:

• 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?
No campo test.coverage de vite.config.ts, configure provider, de preferência v8; reporter, como text, html ou lcov; e thresholds, com os limites mínimos de cobertura. Execute vitest run --coverage para gerar o relatório. Se os limites não forem atingidos, o comando retorna erro, o que permite exigir um nível mínimo de qualidade no ambiente de CI.
Qual é o princípio central do fluxo de TDD?
O princípio central do TDD é o ciclo vermelho-verde-refatorar:

• 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

Trilha de leitura da sérieParte 1 de 1

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.

Ver hub da série

Anterior

Você está no início desta série.

Próximo

Este é o post mais recente da série até agora.

Posts relacionados

Comentários

Entre com GitHub para comentar

Easton BlogEaston Blog