frameworks
May 05, 2026 · 10 min read · 0 views

Vitest 2.0: Fast Unit Testing for Modern JavaScript & TypeScript Projects

Vitest 2.0 brings major performance improvements, better ESM support, and native TypeScript testing. Learn how to migrate and leverage its speed for your test suite.

Introduction

Vitest 2.0 marks a significant milestone for the JavaScript testing ecosystem. As a blazingly-fast unit test framework powered by Vite, Vitest has rapidly become the go-to choice for developers building modern applications with TypeScript, Vue, React, and other frameworks. The 2.0 release doubles down on performance, introduces native TypeScript support without compilation overhead, and streamlines configuration for teams moving away from Jest.

If you’re still running Jest or an older testing setup, or if you’re curious about what makes Vitest stand out, this guide will walk you through the key features, migration paths, and practical examples to get you started.

Why Vitest 2.0 Matters

Speed as a First-Class Feature

Vitest leverages Vite’s instant module serving and hot module replacement (HMR) to eliminate the test compilation bottleneck that plagues Jest and other traditional test runners. In real-world projects, Vitest runs 5–10× faster than Jest on initial runs and offers near-instant re-runs during development.

Why? Jest was built for a different era. It compiles your entire test suite upfront using Babel, waits for the result, and then executes tests. Vitest’s architecture, built on Vite, only transforms what’s needed, when it’s needed.

Native TypeScript Support

Vitest 2.0 now includes native TypeScript transpilation without requiring ts-jest or similar plugins. This means:

  • No extra configuration for .ts and .tsx test files
  • Instant TypeScript parsing and execution
  • Full type inference in test assertions
  • No Babel setup required

ESM-First by Default

Vitest 2.0 fully embraces ES modules. CommonJS is still supported, but the framework prioritizes ESM, making it future-proof and aligned with the JavaScript ecosystem’s direction. This is especially valuable if you’re shipping ESM packages yourself.

Getting Started

Installation

Start by installing Vitest and its peer dependencies:

npm install -D vitest @vitest/ui happy-dom

Breakdown:

  • vitest: The test runner itself
  • @vitest/ui: Optional but highly recommended — a browser-based dashboard for test results
  • happy-dom: A lightweight DOM implementation for component testing (or use jsdom if you need more comprehensive DOM APIs)

Basic Configuration

Create a vitest.config.ts (or vitest.config.js) at your project root:

import { defineConfig } from 'vitest/config';
import vue from '@vitejs/plugin-vue'; // if using Vue

export default defineConfig({
  plugins: [vue()],
  test: {
    globals: true, // enables describe/it/expect without imports
    environment: 'happy-dom', // or 'jsdom' for fuller DOM support
    setupFiles: ['./tests/setup.ts'], // optional: run before all tests
    coverage: {
      provider: 'v8', // built-in coverage
      reporter: ['text', 'json', 'html'],
      lines: 80,
      functions: 80,
      branches: 75,
    },
  },
});

With globals: true, you don’t need to import describe, it, expect, etc. into every test file — a nice quality-of-life improvement over Jest’s defaults.

Your First Test

Create src/math.ts:

export function add(a: number, b: number): number {
  return a + b;
}

export function multiply(a: number, b: number): number {
  return a * b;
}

Now create src/__tests__/math.test.ts:

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

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

  it('should multiply two numbers', () => {
    expect(multiply(4, 5)).toBe(20);
  });

  it('should handle negative numbers', () => {
    expect(add(-1, -1)).toBe(-2);
    expect(multiply(-2, 3)).toBe(-6);
  });
});

Run the tests:

npm run test

Vitest will instantly compile and run your TypeScript tests without any intermediate build step. If you installed @vitest/ui, you can also run:

npm run test -- --ui

This opens a browser-based dashboard at http://localhost:51204/__vitest__/ showing all test results in real-time.

Step-by-Step Guide: Migration from Jest

Step 1: Install Vitest

Install Vitest alongside Jest (don’t remove Jest yet):

npm install -D vitest happy-dom @vitest/ui

Step 2: Create vitest.config.ts

Most Jest configurations can be reused. Here’s a template that covers common Jest patterns:

import { defineConfig } from 'vitest/config';
import path from 'path';

export default defineConfig({
  test: {
    globals: true,
    environment: 'happy-dom',
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
    setupFiles: ['./tests/setup.ts'],
    include: ['src/**/*.{test,spec}.{ts,tsx}'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'html'],
    },
  },
});

Key differences from Jest:

  • testEnvironmentenvironment
  • setupFilesAfterEnvsetupFiles (Vitest runs setup before imports)
  • Module aliases go in Vite’s resolve.alias

Step 3: Update package.json Scripts

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

Step 4: Replace Jest Globals (if applicable)

If you have a Jest setup file, some APIs have moved:

jest.mock() → Still supported, but Vitest prefers explicit imports

Instead of:

jest.mock('../api', () => ({
  fetchUser: () => Promise.resolve({ id: 1 }),
}));

Use Vitest’s vi helper:

import { vi } from 'vitest';

vi.mock('../api', () => ({
  fetchUser: () => Promise.resolve({ id: 1 }),
}));

jest.spyOn()vi.spyOn()

const spy = vi.spyOn(obj, 'method');
expect(spy).toHaveBeenCalled();
spy.mockRestore();

Step 5: Run Tests in Parallel

Vitest runs tests in parallel by default (unlike Jest). If you have tests with side effects or shared state, isolate them:

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

describe.sequential('Database tests', () => {
  // These run sequentially
  it('should insert a record', () => { /* ... */ });
  it('should update the record', () => { /* ... */ });
});

Step 6: Gradual Adoption

You can run both Jest and Vitest in parallel during migration:

{
  "scripts": {
    "test:jest": "jest",
    "test:vitest": "vitest run",
    "test": "npm run test:jest && npm run test:vitest"
  }
}

Once all tests pass in Vitest, remove Jest and its config files.

Advanced Patterns

Mocking and Spying

Vitest’s vi module provides comprehensive mocking utilities:

import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as api from '../api';

describe('User Service', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('should fetch user data', async () => {
    const mockUser = { id: 1, name: 'Alice' };
    vi.spyOn(api, 'getUser').mockResolvedValue(mockUser);

    const user = await api.getUser(1);

    expect(user).toEqual(mockUser);
    expect(api.getUser).toHaveBeenCalledWith(1);
  });

  it('should handle errors', async () => {
    const error = new Error('Network error');
    vi.spyOn(api, 'getUser').mockRejectedValue(error);

    await expect(api.getUser(1)).rejects.toThrow('Network error');
  });
});

Testing Async Code

Vitest handles promises, async/await, and callbacks seamlessly:

import { it, expect } from 'vitest';

it('should resolve with data', async () => {
  const result = await fetchData();
  expect(result).toBeDefined();
});

it('should reject with error', () => {
  return expect(failingFetch()).rejects.toThrow();
});

it('should call callback', (done) => {
  callbackFunction(() => {
    expect(true).toBe(true);
    done();
  });
});

Snapshot Testing

Snapshots work identically to Jest:

it('should render component', () => {
  const html = renderComponent();
  expect(html).toMatchSnapshot();
});

Update snapshots with:

vitest --update

Code Coverage

Generate coverage reports without additional dependencies:

vitest run --coverage

Vitest uses V8 (the JavaScript engine’s native coverage tool) for fast, accurate coverage. Open coverage/index.html to browse results.

Common Pitfalls

1. Test Isolation Issues

Vitest runs tests in parallel by default. If your tests modify global state (e.g., environment variables), use beforeEach and afterEach to clean up:

beforeEach(() => {
  process.env.API_KEY = 'test-key';
});

afterEach(() => {
  delete process.env.API_KEY;
});

2. Module Mocking Path Resolution

When mocking modules, ensure the path matches exactly how the module is imported:

// ❌ Wrong
vi.mock('api'); // if imported as import * as api from './api'

// ✅ Correct
vi.mock('./api');

3. Missing Environment Setup

If testing DOM-dependent code, ensure your config specifies an environment:

test: {
  environment: 'happy-dom', // or 'jsdom'
},

Without this, document and window will be undefined.

4. Skipping Slow Tests in Development

Use it.skip() to temporarily disable slow tests during development:

it.skip('should process large dataset', () => {
  // This is slow; skip it locally
});

Or run only a specific test:

vitest --grep "specific test name"

5. Type Issues with Global Test API

If using globals: true but not getting autocomplete, ensure your tsconfig.json includes Vitest types:

{
  "compilerOptions": {
    "types": ["vitest/globals"]
  }
}

Performance Tips

1. Use happy-dom for Most Tests

If you don’t need full HTML5 compliance, happy-dom is 2–3× faster than jsdom. Switch only components that truly need jsdom:

import { describe, it } from 'vitest';

describe.configure({ environment: 'jsdom' });
// All tests in this block use jsdom

2. Disable Coverage During Development

Coverage collection adds overhead. Enable it only in CI or on-demand:

# Development (fast)
vitest

# Coverage report (slower)
vitest run --coverage

3. Use Workspace Projects for Monorepos

If you have multiple packages, configure Vitest workspaces:

export default defineConfig({
  test: {
    workspace: [
      { root: './packages/core' },
      { root: './packages/utils' },
    ],
  },
});

Practical Example: Testing an API Client

Here’s a real-world scenario — testing an authenticated API client:

// src/api-client.ts
interface FetchOptions {
  headers?: Record<string, string>;
  method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
  body?: unknown;
}

export class ApiClient {
  constructor(private baseUrl: string, private token: string) {}

  async request<T>(endpoint: string, options: FetchOptions = {}): Promise<T> {
    const url = `${this.baseUrl}${endpoint}`;
    const response = await fetch(url, {
      ...options,
      headers: {
        'Authorization': `Bearer ${this.token}`,
        'Content-Type': 'application/json',
        ...options.headers,
      },
    });

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }

    return response.json();
  }

  getUser(id: number) {
    return this.request(`/users/${id}`);
  }
}

Now, the test file:

// src/__tests__/api-client.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ApiClient } from '../api-client';

describe('ApiClient', () => {
  let client: ApiClient;
  const mockFetch = vi.fn();

  beforeEach(() => {
    global.fetch = mockFetch;
    client = new ApiClient('https://api.example.com', 'test-token');
    vi.clearAllMocks();
  });

  it('should include authorization header', async () => {
    mockFetch.mockResolvedValueOnce({
      ok: true,
      json: async () => ({ id: 1, name: 'Alice' }),
    });

    await client.getUser(1);

    expect(mockFetch).toHaveBeenCalledWith(
      'https://api.example.com/users/1',
      expect.objectContaining({
        headers: expect.objectContaining({
          'Authorization': 'Bearer test-token',
        }),
      }),
    );
  });

  it('should throw on non-200 response', async () => {
    mockFetch.mockResolvedValueOnce({
      ok: false,
      status: 404,
      statusText: 'Not Found',
    });

    await expect(client.getUser(999)).rejects.toThrow('HTTP 404');
  });

  it('should parse JSON response', async () => {
    const mockUser = { id: 1, name: 'Alice', email: '[email protected]' };
    mockFetch.mockResolvedValueOnce({
      ok: true,
      json: async () => mockUser,
    });

    const user = await client.getUser(1);

    expect(user).toEqual(mockUser);
  });
});

Run with:

npm run test -- --ui

You’ll see all three tests pass instantly, with real-time re-runs as you edit.

Testing JSON Data with Kloubot Tools

When testing APIs that return JSON, you can use JSON Formatter to validate and pretty-print test fixtures before hardcoding them into your tests. For complex test data, Mock Data Generator can generate realistic fixtures matching your API schema, then paste them directly into your test files.

If your tests involve decoding JWTs returned from authentication endpoints, JWT Decoder lets you inspect token claims during development.

Debugging Tests

Vitest integrates seamlessly with Node debugging tools. Run tests with Inspector:

node --inspect-brk ./node_modules/vitest/vitest.mjs run

Then open chrome://inspect in Chrome DevTools to set breakpoints and step through test code.

Or use the WebStorm/VS Code debugger directly by adding a debug configuration.

Why It Matters for Your Team

  1. Faster Feedback Loop: Test execution in milliseconds vs. seconds accelerates development
  2. Less Configuration: Native TypeScript support eliminates ts-jest, babel-jest complexity
  3. Better DX: Hot module reload for tests, cleaner error messages, built-in UI dashboard
  4. Future-Proof: ESM-first design aligns with JavaScript ecosystem trends
  5. Drop-in Jest Replacement: If you use Jest’s API, migration takes hours, not days

Conclusion

Vitest 2.0 represents a maturation of modern JavaScript tooling. By building on Vite’s proven architecture and embracing TypeScript natively, it solves real pain points in the testing workflow. Whether you’re migrating from Jest, starting a new project, or optimizing your test suite’s performance, Vitest is worth serious consideration.

The ecosystem around Vitest continues to grow—with community plugins for Testing Library, MSW, and more—making it a safe bet for production applications across frameworks.

Start with the configuration template in this guide, run your first test, and experience the speed boost firsthand. You can also use API Request Builder to test your API endpoints during development, and Regex Tester if you’re validating patterns in your test assertions.

This post was generated with AI assistance and reviewed for accuracy.