DEV Community

Saswata Pal
Saswata Pal

Posted on

Why I Chose Vitest Over Jest: 10x Faster Tests & Native ESM Support

Why I Chose Vitest Over Jest: 10x Faster Tests & Native ESM Support

Ever waited 20 seconds for your tests to run, only to realize you made a typo? Yeah, me too. That's why I ditched Jest.

After migrating my React component library to Vitest, my tests went from 18.7 seconds to 1.8 seconds. That's not a typo—it's a 10x improvement. And the best part? I spent less time configuring Vitest than I did fighting with Jest's ESM support.

If you're using Vite and still running Jest, you're leaving serious productivity on the table. Let me show you why.


🎯 The Problem

The Context

I was building a UI component library with:

  • 17 test files: React component tests with Testing Library
  • Build tool: Vite (fast dev server, ESM-native)
  • Tech stack: TypeScript 5.6, React 19 RC, Tailwind v4
  • Test requirements: Unit tests, snapshot tests, coverage reports
  • Development workflow: Watch mode during development
  • CI/CD: Fast test execution on every PR

The Challenge

Jest was fighting against my modern stack:

  • 🐌 Slow startup: 8+ seconds just to initialize Jest
  • 📦 ESM hell: Jest transforms ESM to CommonJS, breaking modules
  • ⚙️ Config complexity: babel-jest, ts-jest, transform configs
  • 🔧 Vite mismatch: Jest uses different resolver than Vite = import errors
  • 🐛 Module mocking: ESM mocks don't work like CommonJS
  • 💥 Dependency issues: jest-environment-jsdom, @types/jest, etc.

Real Pain Example

# The Jest reality:
npm install --save-dev \
  jest \
  @types/jest \
  ts-jest \
  @testing-library/react \
  @testing-library/jest-dom \
  jest-environment-jsdom \
  babel-jest \
  @babel/preset-env \
  @babel/preset-react \
  @babel/preset-typescript
# Total: 10+ packages, multiple config files

# jest.config.js (50+ lines of complexity)
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'jsdom',
  transform: {
    '^.+\\.tsx?$': 'ts-jest',
  },
  moduleNameMapper: {
    '\\.(css|less|scss)$': 'identity-obj-proxy',
    '^@/(.*)$': '<rootDir>/src/$1',
  },
  setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
  // More config...
};

# Run tests:
npm test  # ☕ 8s startup + 12s tests = 20s total

# In watch mode:
npm test -- --watch  # 💥 Re-runs all tests on any change
Enter fullscreen mode Exit fullscreen mode

The Breaking Point

// Modern ESM import in component:
import { Button } from '@/components/Button';

// Vite: ✅ Works perfectly (native ESM)
// Jest: ❌ Error: Cannot find module '@/components/Button'
//       Need to configure moduleNameMapper, transformIgnorePatterns, etc.

// After 2 hours of Jest config:
// Jest: ⚠️ Works but transforms to CommonJS
//       Different behavior than production build 💥
Enter fullscreen mode Exit fullscreen mode

The problem: Jest was built for CommonJS era (2014), fighting against modern ESM/Vite stack.


✅ Evaluation Criteria

Must-Have Requirements

  1. Fast execution - Under 3s for 17 test files
  2. Vite compatibility - Use same config as build tool
  3. ESM support - Native ESM, no transforms needed
  4. TypeScript support - First-class TS without babel/ts-jest
  5. React Testing Library - Works with @testing-library/react

Nice-to-Have Features

  • Watch mode with smart re-runs (only changed tests)
  • UI mode for debugging tests
  • Coverage reports (Istanbul/c8)
  • Snapshot testing
  • Parallel test execution
  • HMR for tests (hot module replacement)

Deal Breakers

  • ❌ Requires complex transform configuration
  • ❌ Doesn't work with Vite imports (path aliases)
  • ❌ Slow startup time (5+ seconds)
  • ❌ Poor ESM support (forced CommonJS transforms)
  • ❌ Different behavior than production build

Scoring Framework

Criteria Weight Why It Matters
Test Speed 30% Run tests 50+ times/day in watch mode
Vite Integration 25% Same config = same behavior as prod
Developer Experience 20% Watch mode, UI, debugging
ESM Support 15% Modern stack requires native ESM
Ecosystem 10% Community, plugins, maturity

🥊 The Contenders

Vitest - Vite-Native Test Runner

  • Best For: Vite projects, modern ESM stack
  • Key Strength: Vite config reuse, blazing fast
  • Key Weakness: Younger ecosystem than Jest
  • GitHub Stars: 12.8k ⭐
  • NPM Downloads: 5M/week 📦
  • First Release: 2021
  • Maintained By: Vitest team (Anthony Fu)
  • Language: TypeScript (powered by Vite/esbuild)
  • Current Version: 4.0.8 (stable, v1.0 in 2023)

Jest - Industry Standard Testing Framework

  • Best For: Legacy projects, maximum compatibility
  • Key Strength: Mature ecosystem, universal adoption
  • Key Weakness: Slow, poor ESM support, complex config
  • GitHub Stars: 44k ⭐
  • NPM Downloads: 20M/week 📦
  • First Release: 2014 (by Facebook)
  • Maintained By: OpenJS Foundation
  • Language: JavaScript (Node.js-based)
  • Current Version: 29.x

Testing Library (Agnostic)

  • Note: Works with both Vitest and Jest
  • Key Strength: User-centric testing approach
  • Use Case: Component testing (React, Vue, etc.)
  • GitHub Stars: 18k ⭐
  • Works With: Jest, Vitest, Mocha, others

uvu - Micro Testing Framework

  • Best For: Tiny projects, minimal overhead
  • Key Strength: Extremely fast, 100KB size
  • Key Weakness: Minimal features, no watch mode
  • GitHub Stars: 3k ⭐
  • NPM Downloads: 1M/week 📦

Mocha + Chai - Classic Combo

  • Best For: Node.js APIs (not React)
  • Key Strength: Flexible, modular
  • Key Weakness: Manual setup, no built-in assertions
  • GitHub Stars: Mocha 22k ⭐, Chai 8k ⭐
  • Status: Maintenance mode

📊 Head-to-Head Comparison

Quick Feature Matrix

Feature Vitest Jest uvu Mocha+Chai
Test Speed ⭐⭐⭐⭐⭐ ⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐
Startup Time ⭐⭐⭐⭐⭐ ⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐
Vite Integration ⭐⭐⭐⭐⭐ ⚠️
ESM Support ⭐⭐⭐⭐⭐ ⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐
TypeScript ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐
Watch Mode ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⚠️
UI Mode ✅ Built-in
Coverage ✅ c8/Istanbul ✅ Istanbul ⚠️ Manual ⚠️ Manual
Snapshot Tests
Mocking ✅ Advanced ✅ Advanced ⚠️ Basic ⚠️ Manual
Parallel Tests ✅ Default ⚠️
Config Complexity ⭐⭐⭐⭐⭐ ⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐
React Testing ✅ Excellent ✅ Excellent ⚠️ Manual ⚠️ Manual
Community Growing Huge Small Medium
Maturity 3 years 10+ years 5 years 12+ years

🔍 Deep Dive: Vitest

What It Is

Vitest is a blazing-fast test runner powered by Vite. It reuses your Vite config for testing, ensuring tests behave identically to your production build. Think "Jest API with Vite speed."

How It Works

Vitest Architecture:

Vite Config (vite.config.ts)
    ↓
Shared with Vitest (vitest.config.ts extends it)
    ↓
Tests use same:
- Module resolution (path aliases work!)
- Transforms (TypeScript, JSX)
- Plugins (same as dev/build)
    ↓
Result: Tests = Production behavior ✅
Enter fullscreen mode Exit fullscreen mode

vs.

Jest Architecture:

vite.config.ts (for build)
jest.config.js (separate config!)
    ↓
Different:
- Module resolution (manual moduleNameMapper)
- Transforms (babel-jest, ts-jest)
- Plugins (jest plugins ≠ Vite plugins)
    ↓
Result: Tests ≠ Production behavior ⚠️
Enter fullscreen mode Exit fullscreen mode

Installation

# Install Vitest
pnpm add -D vitest

# For React testing
pnpm add -D @testing-library/react @testing-library/jest-dom

# For UI mode (optional)
pnpm add -D @vitest/ui

# For coverage (optional)
pnpm add -D @vitest/coverage-v8
Enter fullscreen mode Exit fullscreen mode

Pros ✅

  1. Blazing Fast Execution - 10x faster than Jest

    • Impact: 2s vs 20s for 17 tests (my codebase)
    • Reason: Vite's esbuild transforms, smart caching
    • Use case: Continuous testing in watch mode
  2. Zero Config with Vite - Reuses vite.config.ts

    • Impact: 5 lines vs 50+ lines (Jest)
    • Reason: Inherits all Vite settings
    • Use case: Path aliases, plugins just work
  3. Native ESM Support - No CommonJS transforms

    • Impact: Tests behave like production code
    • Reason: Vite is ESM-first
    • Use case: Modern imports, dynamic imports
  4. Instant Hot Module Replacement - Tests update on save

    • Impact: Sub-second feedback in watch mode
    • Reason: Vite HMR for tests
    • Use case: TDD workflow, rapid iteration
  5. Built-in UI Mode - Visual test runner

    • Impact: Beautiful debugging interface
    • Command: vitest --ui
    • Use case: Debugging failing tests, exploring coverage
  6. Jest-Compatible API - Easy migration

    • Impact: Minimal code changes from Jest
    • Reason: Same describe, it, expect API
    • Use case: Migrate existing Jest tests
  7. TypeScript First-Class - No ts-jest needed

    • Impact: Native TS support via Vite
    • Reason: Vite handles TS transforms
    • Use case: Type-safe tests without config
  8. Smart Watch Mode - Re-runs only affected tests

    • Impact: Instant feedback on changes
    • Reason: Vite's dependency graph
    • Use case: Large test suites

Cons ❌

  1. Younger Ecosystem - 3 years vs Jest's 10+

    • Impact: Fewer Stack Overflow answers
    • Workaround: Excellent docs, active Discord
    • Reality: Covers 95% of Jest features
  2. Vite Dependency - Requires Vite in stack

    • Impact: Not suitable for non-Vite projects
    • Workaround: Can use standalone (less optimal)
    • Reality: If using Vite, perfect fit
  3. Some Jest Features Missing - e.g., jest.spyOn nuances

    • Impact: May need slight API changes when migrating
    • Workaround: Vitest alternatives exist
    • Reality: Rarely an issue for most tests

My Configuration

// vitest.config.ts (18 lines total!)
import { resolve } from 'node:path';
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vitest/config';

export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: ['./src/test-setup.ts'],
    css: true,
  },
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src'),
    },
  },
});
Enter fullscreen mode Exit fullscreen mode
// src/test-setup.ts (3 lines!)
import '@testing-library/jest-dom';
import { expect } from 'vitest';
Enter fullscreen mode Exit fullscreen mode
// package.json scripts
{
  "scripts": {
    "test": "vitest run",
    "test:watch": "vitest --watch",
    "test:ui": "vitest --ui",
    "test:coverage": "vitest run --coverage"
  }
}
Enter fullscreen mode Exit fullscreen mode

Total config complexity: ⭐⭐⭐⭐⭐ (5/5 - Dead simple)


🔍 Deep Dive: Jest

What It Is

Jest is the industry-standard testing framework created by Facebook in 2014. Universal adoption, massive ecosystem, but built for CommonJS era.

Pros ✅

  1. Massive Ecosystem - 1000+ plugins, extensions
  2. Universal Adoption - Every developer knows it
  3. Battle-Tested - 10+ years in production at scale
  4. Extensive Documentation - Answers for everything
  5. Snapshot Testing - Pioneered the concept

Cons ❌

  1. Slow Execution - 10x slower than Vitest (20s vs 2s)
  2. Poor ESM Support - Transforms ESM to CommonJS
  3. Complex Configuration - 50+ lines, multiple tools
  4. Vite Mismatch - Separate config = different behavior
  5. Heavy Dependencies - 10+ packages for full setup
  6. Slow Startup - 8+ seconds just to initialize

Typical Configuration Complexity

// jest.config.js (50+ lines)
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'jsdom',
  roots: ['<rootDir>/src'],
  testMatch: ['**/__tests__/**/*.ts?(x)', '**/?(*.)+(spec|test).ts?(x)'],
  transform: {
    '^.+\\.tsx?$': 'ts-jest',
  },
  moduleNameMapper: {
    '\\.(css|less|scss|sass)$': 'identity-obj-proxy',
    '^@/(.*)$': '<rootDir>/src/$1',
  },
  setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
  collectCoverageFrom: [
    'src/**/*.{ts,tsx}',
    '!src/**/*.d.ts',
    '!src/**/*.stories.tsx',
  ],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80,
    },
  },
  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
  transformIgnorePatterns: [
    'node_modules/(?!(some-esm-package)/)',
  ],
  globals: {
    'ts-jest': {
      tsconfig: {
        jsx: 'react',
      },
    },
  },
};

// package.json dependencies (10+ packages)
{
  "devDependencies": {
    "@testing-library/jest-dom": "^6.0.0",
    "@testing-library/react": "^14.0.0",
    "@types/jest": "^29.0.0",
    "babel-jest": "^29.0.0",
    "identity-obj-proxy": "^3.0.0",
    "jest": "^29.0.0",
    "jest-environment-jsdom": "^29.0.0",
    "ts-jest": "^29.0.0"
  }
}
Enter fullscreen mode Exit fullscreen mode

Total config complexity: ⭐⭐ (2/5 - Very complex)


🔍 Deep Dive: Alternatives

uvu - Micro Testing Framework

Pros:

  • Extremely fast (faster than Vitest)
  • 100KB size (vs Vitest's 2MB)
  • Zero dependencies

Cons:

  • No watch mode
  • No UI
  • Minimal features (manual mocking, no snapshots)
  • Not suitable for React components

Best For: Tiny Node.js libraries, utility functions

Mocha + Chai

Pros:

  • Flexible, modular
  • Good for Node.js APIs

Cons:

  • Maintenance mode (less active development)
  • Manual setup (no built-in assertions)
  • Poor React support
  • Separate tools for mocking (Sinon), assertions (Chai)

Best For: Legacy Node.js projects


🧪 Real-World Testing

My Testing Setup

Machine: MacBook Pro M2, 16GB RAM

Project: @ccl/ui component library

Test Files: 17 files (Avatar, Badge, Button, Card, etc.)

Test Cases: ~50 tests total

Tech Stack: React 19 RC, TypeScript 5.6, Vite

Test Date: December 2025

Test 1: Full Test Suite Execution

# Clean run: all tests from scratch
Enter fullscreen mode Exit fullscreen mode
Framework Run 1 Run 2 Run 3 Average
Vitest 1.8s 1.9s 1.7s 1.8s
Jest 18.2s 19.1s 18.8s 18.7s
uvu 0.4s 0.3s 0.4s 0.4s

Winner: Vitest (10x faster than Jest for React tests)

Note: uvu faster but lacks React/JSX support

Test 2: Startup Time (Cold Start)

# Time to first test execution
Enter fullscreen mode Exit fullscreen mode
Framework Initialization Time
Vitest 0.3s
Jest 8.2s
uvu 0.1s

Winner: Vitest (27x faster startup than Jest)

Test 3: Watch Mode (Single File Change)

# Re-run tests after changing one component
Enter fullscreen mode Exit fullscreen mode
Framework Time to Re-run Tests Re-run
Vitest 0.2s 1 file (smart)
Jest 3.8s All files (dumb)
Jest --watch 2.1s All files

Winner: Vitest (19x faster, smarter re-runs)

Test 4: Coverage Report Generation

# Generate coverage with c8/Istanbul
Enter fullscreen mode Exit fullscreen mode
Framework Coverage Time Report Quality
Vitest 2.8s ✅ Excellent (c8)
Jest 24.1s ✅ Excellent (Istanbul)

Winner: Vitest (8.6x faster coverage)

Test 5: Configuration Complexity

Metric Vitest Jest
Config Files 1 (vitest.config.ts) 2+ (jest.config.js, jest.setup.js)
Config Lines 18 lines 50+ lines
Dependencies 1 package 8+ packages
Setup Time 5 minutes 45 minutes
ESM Config ✅ Native ⚠️ Complex transforms
Vite Alignment ✅ Perfect ❌ Separate config

Winner: Vitest (10x simpler)

Test 6: Developer Experience Features

Feature Vitest Jest
Watch Mode ✅ Smart (affected only) ⚠️ Dumb (all or manual)
UI Mode ✅ Built-in (--ui) ❌ Third-party only
HMR ✅ Instant updates ❌ Full re-run
Debugging ✅ VS Code + UI ✅ VS Code
Error Messages ✅ Clear, colored ✅ Clear
Parallel Execution ✅ Default ✅ Default

Winner: Vitest (better DX features)

Real-World Impact

Before Vitest (using Jest):

  • Test execution: 18.7s
  • Startup time: 8.2s
  • Watch mode feedback: 3.8s
  • Tests per day: 30
  • Daily time wasted: 16 minutes
  • Config maintenance: 2 hours/month (ESM issues, transforms)

After Vitest:

  • Test execution: 1.8s
  • Startup time: 0.3s
  • Watch mode feedback: 0.2s (smart)
  • Tests per day: 30+ (can run more often!)
  • Daily time saved: 14 minutes
  • Config maintenance: 10 minutes/month (version bumps only)

ROI:

  • Time saved: 14 min/day × 20 workdays = 4.7 hours/month
  • At $80/hour = $376/month productivity gain
  • Config simplification = Faster team onboarding
  • Zero ESM configuration hell = Mental peace

🏆 The Decision

I chose Vitest for 4 compelling reasons:

✅ Reason 1: Vite Integration Perfection

My Reality:

  • Using Vite for dev server and production builds
  • Path aliases (@/components/*) throughout codebase
  • Vite plugins for React, TypeScript, Tailwind

Jest Problem:

// Component code (works in Vite):
import { Button } from '@/components/Button';
import styles from './Card.module.css';

// Jest test:
 Cannot find module '@/components/Button'
 Cannot find module './Card.module.css'

// Solution: Configure moduleNameMapper in jest.config.js
moduleNameMapper: {
  '^@/(.*)$': '<rootDir>/src/$1',
  '\\.(css|scss)$': 'identity-obj-proxy',
}

// But now:
⚠️ Different resolution than Vite
⚠️ CSS imports mocked differently
⚠️ Tests pass, but build breaks 💥
Enter fullscreen mode Exit fullscreen mode

Vitest Solution:

// vitest.config.ts (shares Vite config)
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    // ... test config
  },
  // All Vite settings inherited!
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src'),  // Same as vite.config.ts
    },
  },
});

// Result:
 Imports work identically to dev/build
 CSS modules resolve correctly
 Vite plugins work in tests
 Tests = Production behavior
Enter fullscreen mode Exit fullscreen mode

Impact:

  • Zero config duplication
  • Zero import resolution bugs
  • Zero "works in test, breaks in prod" surprises
  • Confidence that tests reflect reality

✅ Reason 2: Performance Game-Changer

My TDD Workflow:

# Typical development session:

1. Write component code
2. Write test
3. Run test (watch mode)
4. See failure
5. Fix code
6. See pass
7. Refactor
8. Run test again
9. Repeat 30+ times/day

# With Jest:
Each cycle: 3.8s feedback
30 cycles = 1.9 minutes waiting per day
Startup: 8.2s every time I restart watch mode
Total: ~16 minutes/day wasted
Flow state: Broken 💥

# With Vitest:
Each cycle: 0.2s feedback (19x faster)
30 cycles = 6 seconds waiting per day
Startup: 0.3s (27x faster)
Total: ~14 minutes/day saved
Flow state: Preserved ✅
Enter fullscreen mode Exit fullscreen mode

Real Example:

// Change Button component:
export const Button = ({ children, ...props }: ButtonProps) => {
  return <button {...props}>{children}</button>;
};

// Save file...

// Jest watch mode:
 3.8 seconds... (waiting, context switched to Twitter)
 Test failed
(Back to code, what was I doing?)

// Vitest watch mode:
 0.2 seconds
 Test failed (immediate feedback, flow preserved)
Fix  Save  0.2s   Pass
Enter fullscreen mode Exit fullscreen mode

Impact:

  • 10x faster test execution (1.8s vs 18.7s)
  • 27x faster startup (0.3s vs 8.2s)
  • 19x faster watch mode (0.2s vs 3.8s)
  • Flow state preserved = Better code quality

✅ Reason 3: Native ESM Support

Modern Stack Reality:

// Modern imports (ESM):
import type { ButtonProps } from '@/types';
import { useState } from 'react';
import { clsx } from 'clsx';

// Dynamic imports:
const Component = await import('./Component');

// Top-level await:
const config = await fetch('/api/config').then(r => r.json());
Enter fullscreen mode Exit fullscreen mode

Jest Approach:

// Jest transforms to CommonJS:
const { useState } = require('react');  // 💥 Not real module behavior

// Dynamic imports broken:
// Top-level await not supported

// ESM support "experimental":
// Add to package.json:
{
  "type": "module",
  "jest": {
    "extensionsToTreatAsEsm": [".ts", ".tsx"],
    "transform": {}
  }
}
// Still buggy, still transforms, still slow
Enter fullscreen mode Exit fullscreen mode

Vitest Approach:

// Native ESM, no transforms:
import { useState } from 'react';  // ✅ Real module
const Component = await import('./Component');  // ✅ Works
const config = await fetch(...);  // ✅ Top-level await works

// Result:
 Tests run exactly like production code
 No CommonJS transforms
 No "experimental" flags
 Just works
Enter fullscreen mode Exit fullscreen mode

Impact:

  • Zero ESM configuration needed
  • Tests behave like production
  • Modern JavaScript features "just work"
  • No transform-related bugs

✅ Reason 4: Developer Experience Features

Vitest UI Mode:

# Start UI mode:
pnpm test:ui

# Opens browser with:
✅ Visual test runner
✅ Coverage visualization
✅ Test file tree
✅ Filter by pass/fail/skipped
✅ Click to run individual tests
✅ Real-time updates
✅ Error stack traces with source code
✅ Performance metrics per test
Enter fullscreen mode Exit fullscreen mode

Impact:

  • Beautiful debugging interface
  • Easier to understand test failures
  • Great for team demos
  • Better than terminal-only

Smart Watch Mode:

# Vitest watch:
Changed: src/components/Button.tsx
Re-running: Button.test.tsx only (1 file)
Time: 0.2s

# Jest watch:
Changed: src/components/Button.tsx
Re-running: All tests (17 files)
Time: 18.7s
Enter fullscreen mode Exit fullscreen mode

Impact:

  • Only re-runs affected tests
  • Instant feedback
  • Can keep watch mode running all day

⚠️ Trade-offs I Accepted

  1. Younger Ecosystem - 3 years vs Jest's 10+

    • Reality: Vitest has 95% of Jest features
    • Community growing rapidly
  2. Vite Dependency - Requires Vite

    • Reality: Already using Vite, perfect fit
    • Not suitable for Webpack projects
  3. Some API Differences - Minor migration effort

    • Reality: Jest-compatible API, minimal changes
    • 1 hour to migrate all tests

The Tipping Point

Testing both frameworks for 1 day, the moment was clear:

With Vitest: Install 1 package, share Vite config, tests fly. Watch mode instant. UI beautiful. Flow state preserved.

With Jest: Install 8 packages, configure transforms, fight ESM, wait 8 seconds to start, wait 4 seconds per change. Flow state destroyed.

For a modern Vite/React project, Vitest was a no-brainer.


🛠️ Implementation Guide

Step 1: Install Vitest (2 minutes)

# Install Vitest
pnpm add -D vitest

# For React testing
pnpm add -D @testing-library/react @testing-library/jest-dom @testing-library/user-event

# For UI mode (optional but recommended)
pnpm add -D @vitest/ui

# For coverage (optional)
pnpm add -D @vitest/coverage-v8
Enter fullscreen mode Exit fullscreen mode

Step 2: Create Vitest Config (3 minutes)

// vitest.config.ts
import { resolve } from 'node:path';
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vitest/config';

export default defineConfig({
  plugins: [react()],
  test: {
    // Enable globals (describe, it, expect, etc.)
    globals: true,

    // Use jsdom for DOM testing
    environment: 'jsdom',

    // Setup file (like Jest's setupFilesAfterEnv)
    setupFiles: ['./src/test-setup.ts'],

    // Enable CSS imports in tests
    css: true,

    // Coverage configuration
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      exclude: [
        'node_modules/',
        'src/test-setup.ts',
        '**/*.stories.tsx',
        '**/*.config.*',
      ],
    },
  },
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src'),
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

Step 3: Create Test Setup File (1 minute)

// src/test-setup.ts
import '@testing-library/jest-dom';
import { expect, afterEach } from 'vitest';
import { cleanup } from '@testing-library/react';

// Cleanup after each test
afterEach(() => {
  cleanup();
});
Enter fullscreen mode Exit fullscreen mode

Step 4: Update package.json Scripts (1 minute)

{
  "scripts": {
    "test": "vitest run",
    "test:watch": "vitest --watch",
    "test:ui": "vitest --ui",
    "test:coverage": "vitest run --coverage"
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 5: Write Your First Test (2 minutes)

// src/components/Button/Button.test.tsx
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { Button } from './Button';

describe('Button', () => {
  it('renders with text', () => {
    render(<Button>Click me</Button>);
    expect(screen.getByRole('button')).toHaveTextContent('Click me');
  });

  it('handles click events', async () => {
    const handleClick = vi.fn();
    render(<Button onClick={handleClick}>Click me</Button>);

    await screen.getByRole('button').click();
    expect(handleClick).toHaveBeenCalledOnce();
  });
});
Enter fullscreen mode Exit fullscreen mode

Step 6: Update TypeScript Config (1 minute)

// tsconfig.json
{
  "compilerOptions": {
    "types": ["vitest/globals", "@testing-library/jest-dom"]
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 7: Run Tests (1 minute)

# Run all tests
pnpm test

# Watch mode (smart re-runs)
pnpm test:watch

# UI mode (visual interface)
pnpm test:ui

# Coverage report
pnpm test:coverage
Enter fullscreen mode Exit fullscreen mode

Total setup time: ⏱️ 10-15 minutes

Migration from Jest (10-20 minutes)

# Step 1: Install Vitest (keep Jest for now)
pnpm add -D vitest @vitest/ui @vitest/coverage-v8

# Step 2: Create vitest.config.ts (see above)

# Step 3: Update imports in test files
# Change:
import { describe, it, expect } from '@jest/globals';
# To:
import { describe, it, expect } from 'vitest';

# Change:
jest.fn()
jest.mock()
# To:
vi.fn()
vi.mock()

# Step 4: Run tests with Vitest
pnpm vitest run

# Step 5: If all pass, remove Jest
pnpm remove jest @types/jest ts-jest jest-environment-jsdom

# Step 6: Delete jest.config.js
rm jest.config.js jest.setup.js

# Step 7: Update scripts in package.json
# Done!
Enter fullscreen mode Exit fullscreen mode

Most tests work without changes! Vitest's Jest-compatible API means 80-90% of tests run as-is.


🔄 When to Choose Differently

Choose Jest If:

  • ✅ Not using Vite (Webpack, Rollup, esbuild directly)
  • ✅ Large existing Jest codebase (migration cost high)
  • ✅ Using Jest-specific plugins (e.g., jest-image-snapshot)
  • ✅ Company policy requires Jest (compliance)
  • ✅ Team deeply trained on Jest ecosystem

Scenario: Legacy React app with 1000+ Jest tests, Webpack build, no plans to migrate to Vite

Choose uvu If:

  • ✅ Testing tiny Node.js libraries (no React)
  • ✅ Need absolute minimal overhead
  • ✅ Only unit tests for functions (no DOM)
  • ✅ Can live without watch mode, coverage, UI

Scenario: 100-line utility library, 10 pure function tests

Choose Mocha+Chai If:

  • ✅ Existing Mocha test suite (don't fix what works)
  • ✅ Testing Node.js APIs only (no React)
  • ✅ Need extreme flexibility (custom reporters, etc.)

Scenario: Legacy Express API with mature Mocha tests

Stick with Jest If:

  • ✅ Current Jest setup works well for your team
  • ✅ Performance is not a bottleneck (<5s test runs)
  • ✅ Not using Vite or planning to migrate

Scenario: Small app, 20 tests, runs in 3 seconds, team happy


🎬 Final Verdict

The Bottom Line

Vitest delivered transformative results:

  • 10x faster than Jest (1.8s vs 18.7s)
  • 27x faster startup (0.3s vs 8.2s)
  • Vite config reuse (zero config duplication)
  • Native ESM (no CommonJS transforms)
  • Smart watch mode (only affected tests)
  • Built-in UI (beautiful debugging interface)
  • Jest-compatible API (easy migration)

ROI:

  • Time saved: 14 min/day = 4.7 hours/month = $376/month (at $80/hour)
  • Config time: 45 min → 10 min setup = $46 saved
  • CI/CD builds: 10x faster = Lower GitHub Actions costs
  • Mental peace: Zero ESM configuration hell = Priceless

My Recommendation

Use Vitest if you:

  • Using Vite (or planning to)
  • Modern TypeScript/React/Vue project
  • Value speed and DX
  • Want tests that behave like production
  • Appreciate beautiful tooling

Use Jest if you:

  • Not using Vite (Webpack, etc.)
  • Large existing Jest codebase (high migration cost)
  • Need Jest-specific plugins
  • Company policy requires it
  • Team strongly prefers familiar tools

1 Month Later: Retrospective

What I got right:

  • Vitest's speed is transformative - 0.2s watch mode feedback
  • Vite config reuse eliminated import bugs completely
  • UI mode better than expected - use it for debugging daily
  • Smart watch mode lets me keep tests running all day

What surprised me:

  • Even faster than benchmarks in real-world use
  • Zero migration issues - all tests worked first try
  • Coverage reports faster and more accurate (c8 > Istanbul)
  • Community responsive despite being younger

What I'd do differently:

  • Migrate sooner! Wasted months fighting Jest's ESM issues

Would I choose it again?

Absolutely, without hesitation. Vitest is the future of testing for Vite projects. The performance alone justifies the switch, but the DX improvements make it a no-brainer.


📚 Resources

Official Documentation

Tools & Extensions

Testing Guides

My Configuration


💬 Your Turn

Which testing framework are you using? Drop a comment:

  • Current setup (Jest? Vitest? Other?)
  • Main pain point (speed? config? ESM?)
  • Build tool (Vite? Webpack? Other?)
  • Would you consider migrating to Vitest?

I'll respond with personalized migration advice! 👇


Next in series: "Why I Chose React 19 RC Over React 18: Concurrent Features & Better DX"

Previous: Why I Chose Biome Over ESLint+Prettier


Last updated: December 3, 2025

Tested with: Vitest 4.0.8, Jest 29.x, React 19 RC

Top comments (0)