Vitest vs Jest: por que migrar os testes do seu projeto JavaScript

1. O cenário atual: Jest como padrão e seus limites

O Jest, criado pelo Facebook em 2014, tornou-se o framework de testes mais popular do ecossistema JavaScript. Sua adoção massiva se deve à experiência integrada — zero configuração para projetos React, asserções embutidas, mocking simplificado e cobertura de código nativa. Milhares de projetos, desde startups até gigantes como Airbnb e Uber, construíram suas suítes de teste sobre ele.

No entanto, à medida que os projetos crescem, as limitações do Jest se tornam evidentes:

  • Cold start lento: Em projetos com centenas ou milhares de arquivos, a primeira execução pode levar dezenas de segundos. O Jest precisa processar todo o grafo de dependências antes de começar.
  • Watch mode ineficiente: O modo de observação do Jest reinicia o processo inteiro a cada alteração, desperdiçando tempo de CPU.
  • Configuração complexa para ESM: Projetos que usam módulos ES modernos exigem plugins adicionais (babel-jest, ts-jest, @jest/globals), aumentando a complexidade do jest.config.js.
  • Isolamento de testes pesado: Cada arquivo de teste roda em um worker separado, mas a comunicação com o processo pai é cara.

2. Vitest: o novo concorrente que nasceu moderno

O Vitest surgiu em 2021 como parte do ecossistema Vite, criado por Anthony Fu e Patak. Sua proposta é simples: oferecer a mesma API do Jest, mas com a velocidade e modernidade do Vite.

Principais características:

  • Compatibilidade nativa com ESM: O Vitest entende módulos ES sem transformação adicional. Seu projeto usa import/export? Funciona direto.
  • API idêntica ao Jest: describe, it, expect, jest.fn()vi.fn(). A migração é quase drop-in.
  • TypeScript e JSX sem plugins: O Vite já lida com TypeScript e JSX nativamente. Não precisa de ts-jest ou babel.
  • CSS Modules: O Vitest processa imports de CSS como objetos vazios ou mocks, sem configuração extra.

3. Comparação prática de performance e velocidade

Vamos comparar um projeto real com 500 arquivos de teste:

Cold start

Jest:

$ jest
PASS  src/__tests__/api.test.ts (12.3s)
PASS  src/__tests__/utils.test.ts (8.7s)
...
Test Suites: 500 passed, 500 total
Time: 47.2s

Vitest:

$ vitest run
PASS  src/__tests__/api.test.ts (1.8s)
PASS  src/__tests__/utils.test.ts (1.2s)
...
Test Suites: 500 passed, 500 total
Time: 12.4s

O Vitest é ~3-4x mais rápido no cold start porque aproveita o cache de módulos do Vite e o sistema de transformação sob demanda.

Watch mode com HMR

Quando você altera um arquivo:

  • Jest: Reinicia todo o processo de teste para o arquivo modificado. Em projetos grandes, pode levar 5-10s para ver o resultado.
  • Vitest: Usa Hot Module Replacement (HMR). Apenas o módulo alterado é recompilado e os testes relevantes são reexecutados. O feedback é quase instantâneo (< 1s).

Execução paralela

O Vitest usa workers baseados no tinypool (fork do jest-worker), mas com melhor gerenciamento de recursos. Em máquinas com múltiplos núcleos, a paralelização é mais eficiente.

4. Compatibilidade e migração gradual

A migração pode ser feita incrementalmente. Veja o mapeamento básico:

Funções globais

Jest Vitest
jest.fn() vi.fn()
jest.mock() vi.mock()
jest.spyOn() vi.spyOn()
jest.useFakeTimers() vi.useFakeTimers()
jest.clearAllMocks() vi.clearAllMocks()

Configuração

jest.config.js (antes):

module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  transform: {
    '^.+\\.tsx?$': 'ts-jest',
  },
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
  },
};

vitest.config.ts (depois):

import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    globals: true,
    environment: 'node',
    alias: {
      '@': '/src',
    },
  },
});

Perceba: sem presets, sem transformadores extras. O Vitest entende TypeScript nativamente.

Migração gradual

  1. Instale vitest e configure o vitest.config.ts
  2. Adicione "test": "vitest run" ao package.json
  3. Execute npx vitest run — ele tentará rodar todos os testes
  4. Ajuste importações: substitua jest por vi nos arquivos de teste
  5. Corrija diferenças de comportamento (veja seção 6)

5. Funcionalidades que o Jest não entrega (ou entrega mal)

Simulação de ambiente de navegador

O @vitest/browser permite testar componentes React/Vue com um navegador real (Playwright) ou headless, sem precisar de jsdom ou happy-dom:

import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    browser: {
      enabled: true,
      provider: 'playwright',
      instances: [{ browser: 'chromium' }],
    },
  },
});

Mocking de módulos ESM

O Jest tem problemas históricos com mocking de módulos ESM. O Vitest resolve isso nativamente:

// __mocks__/api.ts
export const fetchData = vi.fn(() => Promise.resolve({ data: 'mock' }));

// test.ts
vi.mock('@/services/api');

Cobertura de código integrada

O Vitest usa c8 (baseado no V8) ou Istanbul para cobertura, com configuração mínima:

import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      include: ['src/**/*.ts'],
    },
  },
});

vi.hoisted

Uma funcionalidade exclusiva: permite executar código no topo do escopo do módulo, antes de qualquer outra coisa:

import { vi } from 'vitest';

const mockedDate = vi.hoisted(() => new Date('2023-01-01'));
vi.useFakeTimers();
vi.setSystemTime(mockedDate);

6. Casos de borda e armadilhas na migração

Hoisting de vi.mock()

No Jest, jest.mock() é automaticamente hoisted para o topo do arquivo. No Vitest, vi.mock() também é hoisted, mas com diferenças sutis:

// Jest: funciona
jest.mock('fs');
const fs = require('fs');

// Vitest: funciona
vi.mock('fs');
import { readFileSync } from 'fs';

Mas cuidado com variáveis:

// Jest: funciona
const path = '/tmp';
jest.mock('fs', () => ({ readFileSync: () => path }));

// Vitest: NÃO funciona (path não está no escopo)
const path = '/tmp';
vi.mock('fs', () => ({ readFileSync: () => path }));

// Solução: use vi.hoisted
const path = vi.hoisted(() => '/tmp');
vi.mock('fs', () => ({ readFileSync: () => path }));

Timers falsos

// Jest
jest.useFakeTimers();
jest.advanceTimersByTime(1000);

// Vitest
vi.useFakeTimers();
vi.advanceTimersByTime(1000);

Funciona igual, mas o Vitest oferece vi.useFakeTimers({ shouldAdvanceTime: true }) para timers que avançam automaticamente.

Matchers customizados

Se você usa jest-extended, instale @vitest/expect e configure:

import { expect } from 'vitest';
import * as matchers from 'jest-extended';
expect.extend(matchers);

7. Quando NÃO migrar: cenários onde Jest ainda vence

Nem todo projeto deve migrar. Considere manter o Jest se:

  • Projetos legados com plugins Jest profundos: Se você usa jest-jasmine2, jest-circus customizado, ou plugins que dependem de APIs internas do Jest, a migração pode ser trabalhosa.
  • Ambientes corporativos com ferramentas específicas: Algumas plataformas de CI/CD têm integração profunda com Jest (relatórios JUnit, dashboards).
  • Equipes sem familiaridade com Vite: Se seu time não conhece o ecossistema Vite, o custo de aprendizado pode superar os ganhos de performance.
  • Projetos pequenos (< 50 testes): Para projetos pequenos, a diferença de performance é irrelevante. A estabilidade do Jest pode ser mais importante.

8. Decisão final: checklist para avaliar sua migração

Use este roteiro de decisão:

Pergunta 1: Seu projeto usa ESM nativamente?
  Sim → Vá para pergunta 2
  Não → Considere migrar para ESM primeiro ou mantenha Jest

Pergunta 2: O cold start dos testes leva mais de 30 segundos?
  Sim → Vitest é recomendado
  Não → O ganho pode ser pequeno

Pergunta 3: Você depende de plugins Jest não suportados pelo Vitest?
  Sim → Verifique a lista de compatibilidade (jest-extended, jest-chain funcionam)
  Não → Migração é segura

Pergunta 4: Sua equipe conhece Vite?
  Sim → Migre agora
  Não → Invista 1-2 dias em treinamento

Pergunta 5: Você precisa de testes de navegador?
  Sim → Vitest com @vitest/browser é superior
  Não → Ambos atendem

Resultado:
  4-5 "Sim" → Migre imediatamente
  2-3 "Sim" → Migre gradualmente (um pacote por vez)
  0-1 "Sim" → Mantenha Jest por enquanto

Plano de rollout sugerido

  1. Semana 1: Instale Vitest e configure o vitest.config.ts. Execute os testes em paralelo com Jest.
  2. Semana 2: Migre os primeiros 20% dos testes (módulos mais simples).
  3. Semana 3: Migre os 80% restantes. Ajuste casos de borda.
  4. Semana 4: Remova Jest do package.json e comemore a velocidade.

Conclusão

O Vitest não é apenas "Jest, mas mais rápido". É uma reimaginação moderna de como testes deveriam funcionar: nativo para ESM, TypeScript e ecossistema atual. Para projetos novos, a escolha é clara. Para projetos existentes, a migração vale o esforço quando a performance é um gargalo.

A decisão final depende do seu contexto, mas o ecossistema está se movendo rapidamente. O Jest não desaparecerá, mas o Vitest já conquistou seu lugar como a ferramenta de teste padrão para projetos modernos.

Referências