Skip to main content

Command Palette

Search for a command to run...

Vitest: Testing 10x Faster Than Jest

Learn: Vitest: Testing 10x Faster Than Jest

Updated
6 min readView as Markdown
T

Welcome to TopperBlog! 👋

I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.

🎯 What I Write About: • AI/ML Engineering & LLMs • Web3 & Blockchain Development
• System Design & Architecture • Interview Preparation (FAANG) • Freelancing & Remote Work • Modern Tech Stacks (Next.js, React, Rust, TypeScript) • Performance Optimization & Best Practices

💼 Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.

📚 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.

🌐 Let's connect and grow together in this amazing tech journey!

#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering

Vitest: Testing 10x Faster Than Jest

Vite-powered testing that developers love

Modern JavaScript applications demand fast feedback loops. When your test suite takes minutes to run, productivity plummets and developers start skipping tests altogether. Vitest solves this problem by leveraging Vite's lightning-fast Hot Module Replacement (HMR) and native ESM support to deliver test execution speeds that make Jest feel like ancient history.

The Testing Problem

Traditional testing frameworks like Jest were built for a different era. They rely on Node.js's CommonJS module system and require extensive transformation pipelines to handle modern JavaScript features. This creates several pain points:

Slow startup times: Jest needs to transform and bundle your entire codebase before running a single test. For large projects, this initialization can take 30+ seconds.

Configuration complexity: Getting Jest to work with TypeScript, JSX, path aliases, and modern ES features requires configuring Babel, ts-jest, and various other plugins. Each addition slows things down further.

Watch mode inefficiency: Even in watch mode, Jest often re-runs more tests than necessary because it can't accurately track module dependencies.

Inconsistent environments: Your Vite development environment uses one set of transformations while Jest uses another, leading to "works in dev but fails in tests" scenarios.

These issues compound as projects grow. A test suite that takes 5 minutes to run means developers wait an hour per day just watching tests execute. That's 250 hours per year per developer—pure waste.

Why This Tool Wins

Vitest was designed from the ground up to work seamlessly with Vite, inheriting all its performance optimizations while providing a Jest-compatible API that makes migration painless.

Native speed: Vitest uses Vite's transformation pipeline, which means your tests run with the same blazing-fast speed as your dev server. First-run times drop from 30+ seconds to under 3 seconds for most projects.

Smart watch mode: Thanks to Vite's dependency graph, Vitest knows exactly which tests to re-run when files change. Modify a utility function and only the 3 tests that import it will execute—not your entire 500-test suite.

Zero configuration: If you're already using Vite, Vitest works out of the box. It reads your vite.config.ts and respects all your path aliases, plugins, and transformations automatically.

True ESM support: Unlike Jest's experimental ESM mode, Vitest handles ES modules natively. No more --experimental-vm-modules flags or mysterious import errors.

Modern features built-in: TypeScript, JSX, CSS imports, JSON imports—everything works without additional configuration. Vitest even supports importing .vue, .svelte, and other framework-specific files directly in tests.

Concurrent by default: Tests run in parallel using worker threads, maximizing CPU utilization. A 2-minute Jest suite often completes in 10-15 seconds with Vitest.

Getting Started

Installation takes seconds. Add Vitest to your existing Vite project:

npm install -D vitest

Add a test script to package.json:

{
  "scripts": {
    "test": "vitest",
    "test:ui": "vitest --ui"
  }
}

Create your first test file src/utils/math.test.ts:

import { describe, it, expect } from 'vitest'
import { add, multiply } from './math'

describe('Math utilities', () => {
  it('adds two numbers correctly', () => {
    expect(add(2, 3)).toBe(5)
    expect(add(-1, 1)).toBe(0)
  })

  it('multiplies two numbers correctly', () => {
    expect(multiply(3, 4)).toBe(12)
    expect(multiply(0, 100)).toBe(0)
  })
})

Run tests:

npm test

That's it. No configuration files, no setup scripts, no babel plugins. If you need custom configuration, create vitest.config.ts:

import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: './tests/setup.ts',
    coverage: {
      provider: 'v8',
      reporter: ['text', 'html']
    }
  }
})

Best Practices

Use test.concurrent for independent tests: Speed up execution by running independent tests simultaneously:

import { describe, test } from 'vitest'

describe('API endpoints', () => {
  test.concurrent('GET /users returns user list', async () => {
    // Test implementation
  })

  test.concurrent('GET /posts returns posts', async () => {
    // Test implementation
  })
})

Leverage in-source testing: Vitest supports defining tests alongside your source code, improving colocation:

// src/utils/format.ts
export function formatCurrency(amount: number): string {
  return `$${amount.toFixed(2)}`
}

if (import.meta.vitest) {
  const { it, expect } = import.meta.vitest

  it('formats currency correctly', () => {
    expect(formatCurrency(10)).toBe('$10.00')
    expect(formatCurrency(99.9)).toBe('$99.90')
  })
}

Use bench for performance testing: Vitest includes built-in benchmarking:

import { bench, describe } from 'vitest'

describe('Array operations', () => {
  bench('Array.push', () => {
    const arr = []
    for (let i = 0; i < 1000; i++) arr.push(i)
  })

  bench('Array spread', () => {
    let arr = []
    for (let i = 0; i < 1000; i++) arr = [...arr, i]
  })
})

Enable UI mode for debugging: The built-in UI provides visual test exploration:

npm run test:ui

This opens a browser interface showing test results, execution times, and detailed error traces with source maps.

Real Examples

Testing React components with Testing Library:

import { render, screen, fireEvent } from '@testing-library/react'
import { describe, it, expect } from 'vitest'
import { Counter } from './Counter'

describe('Counter component', () => {
  it('increments count when button clicked', () => {
    render(<Counter initialCount={0} />)

    const button = screen.getByRole('button', { name: /increment/i })
    const count = screen.getByTestId('count')

    expect(count).toHaveTextContent('0')

    fireEvent.click(button)
    expect(count).toHaveTextContent('1')

    fireEvent.click(button)
    expect(count).toHaveTextContent('2')
  })
})

Mocking API calls with vi utilities:

import { describe, it, expect, vi, beforeEach } from 'vitest'
import { fetchUserData } from './api'

// Mock fetch globally
global.fetch = vi.fn()

describe('fetchUserData', () => {
  beforeEach(() => {
    vi.clearAllMocks()
  })

  it('returns user data on success', async () => {
    const mockUser = { id: 1, name: 'John' }

    global.fetch.mockResolvedValueOnce({
      ok: true,
      json: async () => mockUser
    })

    const result = await fetchUserData(1)

    expect(result).toEqual(mockUser)
    expect(fetch).toHaveBeenCalledWith('/api/users/1')
  })

  it('throws error on failure', async () => {
    global.fetch.mockResolvedValueOnce({
      ok: false,
      status: 404
    })

    await expect(fetchUserData(999)).rejects.toThrow('User not found')
  })
})

Testing async operations with proper cleanup:

import { describe, it, expect, vi } from 'vitest'

describe('Debounced search', () => {
  it('only calls search after delay', async () => {
    vi.useFakeTimers()
    const searchFn = vi.fn()
    const debouncedSearch = debounce(searchFn, 300)

    debouncedSearch('test')
    expect(searchFn).not.toHaveBeenCalled()

    vi.advanceTimersByTime(200)
    expect(searchFn).not.toHaveBeenCalled()

    vi.advanceTimersByTime(100)
    expect(searchFn).toHaveBeenCalledWith('test')

    vi.useRealTimers()
  })
})

Common Pitfalls

Forgetting to await async tests: Vitest won't wait for unhandled promises:

// ❌ Wrong - test passes even if assertion fails
it('fetches data', () => {
  fetchData().then(data => {
    expect(data).toBeDefined()
  })
})

// ✅ Correct - properly awaited
it('fetches data', async () => {
  const data = await fetchData()
  expect(data).toBeDefined()
})

Not cleaning up mocks: Mocks persist between tests unless cleared:

import { beforeEach, vi } from 'vitest'

beforeEach(() => {
  vi.clearAllMocks() // Reset all mocks before each test
})

Incorrect environment configuration: DOM tests need jsdom:

// vitest.config.ts
export default defineConfig({
  test: {
    environment: 'jsdom' // Required for DOM APIs
  }
})

Ignoring coverage gaps: Enable coverage to find untested code:

vitest --coverage

Wrap Up

Vitest represents the next generation of JavaScript testing. By building on Vite's modern architecture, it delivers the speed and developer experience that contemporary applications demand. The Jest-compatible API means migration is straightforward, while the performance improvements are immediately noticeable.

For new projects, Vitest should be your default choice. For existing projects, the migration effort pays for itself within weeks through improved productivity. When your tests run in seconds instead of minutes, you'll wonder how you ever tolerated the old way.

Start with a small test file, experience the speed difference, and you'll never look back. Your future self—and your team—will thank you.