← All articles
TESTING Vitest: Fast Unit Testing for Vite-Based JavaScript ... 2026-03-04 · 4 min read · vitest · testing · unit-testing

Vitest: Fast Unit Testing for Vite-Based JavaScript Projects

Testing 2026-03-04 · 4 min read vitest testing unit-testing vite javascript typescript

Jest has been the default JavaScript test runner for years. But it was designed for CommonJS and requires significant configuration to handle modern ESM, TypeScript, and Vite projects. Vitest was built from the start for the modern JavaScript ecosystem.

Vitest reuses your existing vite.config.ts, supports ESM and TypeScript out of the box, and runs tests in parallel with a watch mode that's dramatically faster than Jest's.

Why Vitest Over Jest?

Feature Vitest Jest
ESM support Native Requires transform config
TypeScript Native Requires ts-jest or Babel
Vite config reuse Yes No
Watch mode speed Very fast (HMR-based) Moderate
Compatible API Yes (Jest-compatible)
Browser testing Yes (experimental) No
Snapshot testing Yes Yes

If you're using Vite already, Vitest is a near-zero-config drop-in. If you're using Jest, migration is usually minimal — Vitest's API is intentionally compatible.

Installation

# npm
npm install -D vitest

# pnpm
pnpm add -D vitest

# bun
bun add -d vitest

Add a test script to package.json:

{
  "scripts": {
    "test": "vitest",
    "test:run": "vitest run",
    "test:coverage": "vitest run --coverage"
  }
}

That's it — no config file needed for basic usage.

Your First Test

Vitest uses the same API as Jest: describe, it/test, expect, beforeEach, afterEach, etc.

// src/utils/math.ts
export function add(a: number, b: number): number {
  return a + b;
}

export function divide(a: number, b: number): number {
  if (b === 0) throw new Error('Division by zero');
  return a / b;
}
// src/utils/math.test.ts
import { describe, it, expect } from 'vitest';
import { add, divide } from './math';

describe('math utilities', () => {
  it('adds two numbers', () => {
    expect(add(2, 3)).toBe(5);
  });

  it('divides two numbers', () => {
    expect(divide(10, 2)).toBe(5);
  });

  it('throws on division by zero', () => {
    expect(() => divide(10, 0)).toThrow('Division by zero');
  });
});

Run with vitest (watch mode) or vitest run (single pass).

Configuration

Vitest can be configured in vite.config.ts or a dedicated vitest.config.ts:

// vite.config.ts
import { defineConfig } from 'vite';
import { defineConfig as defineTestConfig } from 'vitest/config';

export default defineConfig({
  // Your existing Vite config...
  test: {
    // Vitest options
    globals: true,           // Use global expect/describe without imports
    environment: 'node',     // or 'jsdom', 'happy-dom'
    include: ['**/*.{test,spec}.{ts,tsx,js,jsx}'],
    exclude: ['node_modules', 'dist'],
    coverage: {
      provider: 'v8',        // or 'istanbul'
      reporter: ['text', 'html', 'lcov'],
      exclude: ['node_modules', '**/*.test.ts'],
    },
  },
});

Test Environments

Vitest supports multiple environments:

Node (default): For server-side code, utilities, and anything without DOM dependencies.

jsdom: Simulates a browser DOM. Good for testing React/Vue components without a real browser.

npm install -D jsdom
// vitest.config.ts
export default defineConfig({
  test: {
    environment: 'jsdom',
  },
});

happy-dom: Faster jsdom alternative with good compatibility:

npm install -D happy-dom

You can also set the environment per-file using a comment:

// @vitest-environment jsdom
import { describe, it, expect } from 'vitest';

Mocking

Vitest has a built-in mocking system:

Module mocks:

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

// Mock an entire module
vi.mock('./database', () => ({
  getUser: vi.fn().mockResolvedValue({ id: 1, name: 'Alice' }),
}));

// Import after mocking
import { getUser } from './database';

describe('user service', () => {
  it('fetches a user', async () => {
    const user = await getUser(1);
    expect(user.name).toBe('Alice');
    expect(getUser).toHaveBeenCalledWith(1);
  });
});

Spy on existing functions:

import { vi, describe, it, expect, afterEach } from 'vitest';
import * as fs from 'fs';

describe('file operations', () => {
  afterEach(() => vi.restoreAllMocks());

  it('reads a file', () => {
    const spy = vi.spyOn(fs, 'readFileSync').mockReturnValue('mocked content');
    const result = fs.readFileSync('test.txt', 'utf-8');
    expect(result).toBe('mocked content');
    expect(spy).toHaveBeenCalled();
  });
});

Fake timers:

import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';

describe('debounce', () => {
  beforeEach(() => vi.useFakeTimers());
  afterEach(() => vi.useRealTimers());

  it('delays execution', () => {
    const fn = vi.fn();
    const debounced = debounce(fn, 300);

    debounced();
    expect(fn).not.toHaveBeenCalled();

    vi.advanceTimersByTime(300);
    expect(fn).toHaveBeenCalledOnce();
  });
});

Snapshot Testing

Vitest supports Jest-compatible snapshots:

import { describe, it, expect } from 'vitest';
import { renderComponent } from './utils';

describe('Button component', () => {
  it('renders correctly', () => {
    const html = renderComponent('<Button label="Click me" />');
    expect(html).toMatchSnapshot();
  });
});

First run creates the snapshot file. Subsequent runs compare against it. Update with vitest --update-snapshots.

Coverage Reports

npm install -D @vitest/coverage-v8

Run with coverage:

vitest run --coverage

Output in the coverage/ directory. Open coverage/index.html for a detailed breakdown by file and line.

Watch Mode

One of Vitest's best features is its watch mode, which uses Vite's module graph to only re-run tests affected by changed files.

vitest  # starts in watch mode

Press h in watch mode for commands:

Testing React Components

With @testing-library/react:

npm install -D @testing-library/react @testing-library/jest-dom jsdom
// vitest.config.ts
export default defineConfig({
  test: {
    environment: 'jsdom',
    setupFiles: ['./src/test/setup.ts'],
  },
});
// src/test/setup.ts
import '@testing-library/jest-dom';
// src/components/Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { Button } from './Button';

describe('Button', () => {
  it('calls onClick when clicked', () => {
    const onClick = vi.fn();
    render(<Button label="Click me" onClick={onClick} />);

    fireEvent.click(screen.getByText('Click me'));
    expect(onClick).toHaveBeenCalledOnce();
  });
});

Migrating from Jest

Vitest's API is Jest-compatible. Most migrations are just:

  1. Replace jest imports with vitest: import { vi } from 'vitest' instead of using global jest
  2. Replace jest.fn() with vi.fn(), jest.mock with vi.mock, etc.
  3. Remove Jest-specific transform config from your build tool

For TypeScript projects using ts-jest, Vitest is often faster to configure and runs faster — no transpilation step needed.

Summary

Vitest is the right default for modern JavaScript and TypeScript projects, especially those using Vite. The zero-config setup, native ESM and TypeScript support, and fast watch mode remove most of the friction from writing and running tests.

Start with vitest in watch mode while developing — the instant feedback loop makes test-driven development practical rather than painful.