DevToolBoxFREE
Blog

Vitest Complete Guide: Fast Unit Testing for Modern JavaScript & TypeScript (2026)

18 min readby DevToolBox Team

TL;DR

Vitest is a Vite-native unit testing framework that reuses your Vite config for instant HMR-powered test execution. It provides a Jest-compatible API (describe, it, expect) with native ESM support, TypeScript out of the box, and built-in mocking, snapshot testing, code coverage, and benchmarking. This guide covers everything from basic setup to advanced patterns like workspace testing, in-source testing, and CI/CD integration.

Testing JavaScript and TypeScript applications has traditionally been dominated by Jest. While Jest is excellent, it was built before ES modules, Vite, and native TypeScript became mainstream. Configuring Jest for modern projects often requires babel-jest, ts-jest, and custom transform mappings that duplicate your build tool configuration. Vitest solves this by building directly on top of Vite.

This guide covers everything you need to know about Vitest in 2026: from initial setup to advanced patterns like workspace testing, in-source testing, benchmarking, and CI/CD integration. Every section includes practical, copy-paste code examples.

1. What Is Vitest and Why Use It Over Jest?

Vitest is a blazing-fast unit testing framework powered by Vite. It reuses your existing vite.config.ts for transforms, resolve aliases, and plugins, which means zero duplicate configuration. Here is what makes Vitest stand out:

  • Vite-native: Shares the same transform pipeline as your dev server and build tool.
  • Jest-compatible API: describe, it, expect, vi work the same way.
  • Native ESM: No CJS/ESM interop issues. Import/export works out of the box.
  • TypeScript built-in: No ts-jest or @swc/jest needed.
  • HMR watch mode: Only re-runs tests affected by your changes.
  • Built-in coverage: v8 and istanbul providers with zero extra config.
  • Snapshot testing: Inline and file snapshots, same as Jest.
  • Benchmarking: Built-in vitest bench command.
  • Workspace support: Different configs per package in monorepos.

Vitest vs Jest: Quick Comparison

FeatureJestVitest
ESM SupportExperimental flagNative
TypeScriptts-jest / @swc/jestBuilt-in via Vite
Configjest.config.jsReuses vite.config.ts
Watch ModeFile-basedHMR-based (faster)
CoverageSeparate packageBuilt-in (v8 / istanbul)
BenchmarkingNot includedBuilt-in (vitest bench)
In-source TestingNot supportedSupported
Workspaceprojects arrayvitest.workspace.ts
Concurrent TestsPer-file onlyPer-test with .concurrent

2. Installation and Configuration

Getting started with Vitest takes under a minute. Install it as a dev dependency and add a test script:

# Install Vitest
npm install -D vitest

# Or with pnpm / yarn
pnpm add -D vitest
yarn add -D vitest

Add test scripts to your package.json:

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

Vitest reads your vite.config.ts automatically. To add test-specific options, use the test key:

/// <reference types="vitest" />
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    globals: true,
    environment: 'node',
    include: ['src/**/*.{test,spec}.{ts,tsx}'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'html', 'lcov'],
    },
  },
});

When globals: true is set, describe, it, expect, and vi are available globally without importing. Add vitest/globals to your tsconfig.json types for proper IntelliSense:

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

3. Writing Tests: describe, it, expect

Vitest uses the same BDD-style API as Jest. Here is a complete example testing a utility function:

// src/utils/math.test.ts
import { describe, it, expect } from 'vitest';
import { add, divide } from './math';

describe('add', () => {
  it('adds two positive numbers', () => {
    expect(add(2, 3)).toBe(5);
  });
  it('handles negative numbers', () => {
    expect(add(-1, -2)).toBe(-3);
  });
});

describe('divide', () => {
  it('divides two numbers', () => {
    expect(divide(10, 2)).toBe(5);
  });
  it('throws on division by zero', () => {
    expect(() => divide(10, 0)).toThrow('Division by zero');
  });
});

Common Matchers

Vitest provides all the matchers you know from Jest:

expect(value).toBe(42);              // strict ===
expect(obj).toEqual({ a: 1 });       // deep equality
expect(value).toBeTruthy();           // truthy check
expect(value).toBeNull();             // null check
expect(0.1 + 0.2).toBeCloseTo(0.3);  // floating point
expect(str).toMatch(/regex/);         // regex match
expect(str).toContain('sub');         // substring
expect(arr).toHaveLength(3);          // array length
expect(obj).toHaveProperty('key');    // property check
expect(() => fn()).toThrow('msg');     // exception

Test Lifecycle Hooks

beforeAll(async () => { await setupDatabase(); });
afterAll(async () => { await teardownDatabase(); });
beforeEach(() => { resetState(); });
afterEach(() => { cleanup(); });

4. Mocking: vi.fn(), vi.mock(), vi.spyOn()

Vitest provides a powerful mocking system through the vi object. It supports function mocks, module mocks, and spies.

vi.fn() - Mock Functions

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

describe('callback handling', () => {
  it('calls the callback with correct args', () => {
    const callback = vi.fn();

    // Use the mock
    callback('hello', 42);
    callback('world');

    // Assertions
    expect(callback).toHaveBeenCalledTimes(2);
    expect(callback).toHaveBeenCalledWith('hello', 42);
    expect(callback).toHaveBeenLastCalledWith('world');
  });

  it('can define return values', () => {
    const getPrice = vi.fn()
      .mockReturnValueOnce(9.99)
      .mockReturnValueOnce(19.99)
      .mockReturnValue(0);

    expect(getPrice()).toBe(9.99);
    expect(getPrice()).toBe(19.99);
    expect(getPrice()).toBe(0);  // default
  });

  it('can mock implementations', () => {
    const multiply = vi.fn()
      .mockImplementation((a: number, b: number) => a * b);

    expect(multiply(3, 4)).toBe(12);
  });
});

vi.mock() - Module Mocks

// Automatically mock an entire module
vi.mock('./api-client');

// vi.mock calls are hoisted to the top of the file
// so this works even though the import is above
import { fetchUser } from './api-client';

describe('user service', () => {
  it('calls fetchUser with the correct id', async () => {
    // Type-safe access to mock
    const mockedFetch = vi.mocked(fetchUser);
    mockedFetch.mockResolvedValue({ id: 1, name: 'Alice' });

    const user = await fetchUser(1);
    expect(user.name).toBe('Alice');
    expect(mockedFetch).toHaveBeenCalledWith(1);
  });
});

// Partial mock with factory function
vi.mock('./utils', () => ({
  formatDate: vi.fn(() => '2026-01-01'),
  // Keep original implementation for other exports
  ...vi.importActual('./utils'),
}));

vi.spyOn() - Spying on Methods

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

describe('spying', () => {
  afterEach(() => {
    vi.restoreAllMocks();
  });

  it('spies on console.log', () => {
    const spy = vi.spyOn(console, 'log');

    console.log('hello');

    expect(spy).toHaveBeenCalledWith('hello');
  });

  it('spies on and replaces implementation', () => {
    const spy = vi.spyOn(Math, 'random')
      .mockReturnValue(0.5);

    expect(Math.random()).toBe(0.5);
    expect(spy).toHaveBeenCalled();
  });
});

5. Testing Async Code

Vitest handles promises, async/await, callbacks, timers, and fetch calls seamlessly.

Promises and async/await

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

async function fetchTodo(id: number) {
  const res = await fetch(
    'https://jsonplaceholder.typicode.com/todos/' + id
  );
  return res.json();
}

describe('async tests', () => {
  it('resolves a promise', async () => {
    const result = await Promise.resolve(42);
    expect(result).toBe(42);
  });

  it('rejects with an error', async () => {
    await expect(
      Promise.reject(new Error('fail'))
    ).rejects.toThrow('fail');
  });
});

Fake Timers

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

  it('only calls fn after delay', () => {
    const fn = vi.fn();
    const debounced = debounce(fn, 300);

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

    vi.advanceTimersByTime(300);
    expect(fn).toHaveBeenCalledTimes(1);
  });
});

Mocking Fetch

it('mocks a GET request', async () => {
  const mockData = { id: 1, title: 'Test Todo' };
  global.fetch = vi.fn().mockResolvedValue({
    ok: true,
    json: () => Promise.resolve(mockData),
  });
  const res = await fetch('/api/todos/1');
  const data = await res.json();
  expect(data).toEqual(mockData);
  expect(fetch).toHaveBeenCalledWith('/api/todos/1');
});

6. Snapshot Testing

Snapshot tests capture the output of a function or component and compare it against a stored reference. If the output changes, the test fails until you update the snapshot.

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

function generateConfig(env: string) {
  return {
    apiUrl: env === 'production'
      ? 'https://api.example.com'
      : 'http://localhost:3000',
    debug: env !== 'production',
    logLevel: env === 'production' ? 'error' : 'debug',
  };
}

describe('generateConfig', () => {
  // File snapshot (saved to __snapshots__/)
  it('matches production config snapshot', () => {
    expect(generateConfig('production')).toMatchSnapshot();
  });

  // Inline snapshot (stored in the test file)
  it('matches dev config inline snapshot', () => {
    expect(generateConfig('development')).toMatchInlineSnapshot();
    // Vitest fills in the snapshot on first run
  });
});

// Update snapshots: vitest run --update
// Or press u in watch mode

7. Code Coverage with v8 / Istanbul

Vitest has built-in coverage support. Install the provider package and configure thresholds:

# Install the coverage provider
npm install -D @vitest/coverage-v8
# Or for istanbul:
npm install -D @vitest/coverage-istanbul
// vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    coverage: {
      provider: 'v8',  // or 'istanbul'
      reporter: ['text', 'html', 'lcov', 'json'],
      include: ['src/**/*.ts'],
      exclude: [
        'src/**/*.test.ts',
        'src/**/*.d.ts',
        'src/types/**',
      ],
      thresholds: {
        branches: 80,
        functions: 80,
        lines: 80,
        statements: 80,
      },
    },
  },
});

Run coverage with:

# Generate coverage report
npx vitest run --coverage

# Open the HTML report
open coverage/index.html

The v8 provider is faster because it uses V8 engine built-in coverage. The istanbul provider instruments code at the AST level and is more accurate for edge cases like branch coverage in ternary expressions.

8. Testing React and Vue Components

Vitest works seamlessly with @testing-library for component testing. Set the environment to jsdom or happy-dom in your config.

React Component Testing

# Install dependencies
npm install -D @testing-library/react @testing-library/jest-dom \
  @testing-library/user-event jsdom

# Setup: vitest.config.ts
# plugins: [react()], test: { environment: 'jsdom', setupFiles: ['./src/test/setup.ts'] }
# src/test/setup.ts: import '@testing-library/jest-dom/vitest';
// src/components/Counter.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect } from 'vitest';
import { Counter } from './Counter';

describe('Counter', () => {
  it('renders initial count of 0', () => {
    render(<Counter />);
    expect(screen.getByTestId('count')).toHaveTextContent('Count: 0');
  });

  it('increments on click', async () => {
    render(<Counter />);
    const user = userEvent.setup();
    await user.click(screen.getByText('Increment'));
    await user.click(screen.getByText('Increment'));
    expect(screen.getByTestId('count')).toHaveTextContent('Count: 2');
  });
});

// Vue: npm install -D @testing-library/vue happy-dom
// Config: plugins: [vue()], test: { environment: 'happy-dom' }

9. In-Source Testing

Vitest supports writing tests directly inside your source files. The tests are tree-shaken out of production builds, so they have zero impact on bundle size.

// src/utils/slug.ts
export function slugify(text: string): string {
  return text
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/^-|-\$/g, '');
}

// In-source tests — only run by Vitest
if (import.meta.vitest) {
  const { describe, it, expect } = import.meta.vitest;

  describe('slugify', () => {
    it('converts text to slug', () => {
      expect(slugify('Hello World')).toBe('hello-world');
    });

    it('removes special characters', () => {
      expect(slugify('Hello, World!')).toBe('hello-world');
    });

    it('trims leading/trailing hyphens', () => {
      expect(slugify('--hello--')).toBe('hello');
    });
  });
}

Enable in-source testing in your config:

// vitest.config.ts
export default defineConfig({
  test: {
    includeSource: ['src/**/*.ts'],
  },
  define: {
    // Tree-shake in production
    'import.meta.vitest': 'undefined',
  },
});

10. Workspace and Monorepo Testing

For monorepos with packages that need different test environments, Vitest workspaces let you define per-project configurations in a single file:

// vitest.workspace.ts
import { defineWorkspace } from 'vitest/config';

export default defineWorkspace([
  {
    test: {
      name: 'api',
      root: './packages/api',
      environment: 'node',
      include: ['src/**/*.test.ts'],
    },
  },
  {
    test: {
      name: 'web',
      root: './packages/web',
      environment: 'jsdom',
      include: ['src/**/*.test.tsx'],
      setupFiles: ['./src/test/setup.ts'],
    },
  },
]);

Run tests for a specific project:

# Run all workspace tests
npx vitest

# Run only the web project
npx vitest --project web

# Run only the api project
npx vitest --project api

11. Watch Mode and Test Filtering

Vitest runs in watch mode by default during development. It uses Vite HMR to detect which tests are affected by your changes and only re-runs those tests.

# Start watch mode (default)
npx vitest

# Run tests matching a name pattern
npx vitest -t "should validate email"

# Run only specific test files
npx vitest src/utils/math.test.ts

# Run tests matching a file pattern
npx vitest --reporter=verbose utils

# Run in single-run mode (no watch)
npx vitest run

In watch mode, you can use these keyboard shortcuts:

KeyAction
aRe-run all tests
fRe-run only failed tests
uUpdate snapshots
pFilter by filename
tFilter by test name
qQuit watch mode
hShow help

You can also use .only, .skip, .todo, and .concurrent modifiers:

describe('my suite', () => {
  it.only('runs only this test', () => { /* ... */ });
  it.skip('skipped test', () => { /* ... */ });
  it.todo('not implemented yet');
  it.concurrent('runs in parallel', async () => { /* ... */ });
});

12. Benchmarking with vitest bench

Vitest includes a built-in benchmarking tool powered by Tinybench. Create .bench.ts files and run them with vitest bench:

// src/utils/search.bench.ts
import { bench, describe } from 'vitest';

function linearSearch(arr: number[], target: number) {
  for (let i = 0; i < arr.length; i++) {
    if (arr[i] === target) return i;
  }
  return -1;
}

function binarySearch(arr: number[], target: number) {
  let lo = 0, hi = arr.length - 1;
  while (lo <= hi) {
    const mid = (lo + hi) >>> 1;
    if (arr[mid] === target) return mid;
    if (arr[mid] < target) lo = mid + 1;
    else hi = mid - 1;
  }
  return -1;
}

const sorted = Array.from({ length: 10000 }, (_, i) => i);

describe('search algorithms', () => {
  bench('linear search', () => {
    linearSearch(sorted, 9999);
  });

  bench('binary search', () => {
    binarySearch(sorted, 9999);
  });
});
# Run benchmarks
npx vitest bench

# Output shows ops/sec, min, max, mean, p75, p99 for each bench

13. CI/CD Integration

Running Vitest in CI requires non-watch mode and proper reporting. Here are configurations for the most popular CI systems:

GitHub Actions

# .github/workflows/test.yml
name: Tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm
      - run: npm ci
      - run: npx vitest run --reporter=github-actions --coverage
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: coverage-report
          path: coverage/

Test Sharding and Reporters

# Split tests across parallel CI jobs
npx vitest run --shard=1/4  # Job 1 of 4
npx vitest run --shard=2/4  # Job 2 of 4

# JUnit XML for CI dashboards
npx vitest run --reporter=junit --outputFile=test-results.xml

# Multiple reporters simultaneously
npx vitest run --reporter=default --reporter=junit \
  --outputFile.junit=test-results.xml

14. Best Practices

File Organization

Co-locate tests with source files (math.ts + math.test.ts) and keep shared test setup in a src/test/ directory. Use .bench.ts for benchmark files alongside their source.

Testing Patterns

// 1. Arrange-Act-Assert pattern
it('calculates total price', () => {
  const items = [{ price: 9.99, qty: 2 }, { price: 24.99, qty: 1 }];
  const total = calculateTotal(items);
  expect(total).toBeCloseTo(44.97, 2);
});

// 2. Parameterized tests with test.each
it.each([
  [1, 1, 2], [2, 3, 5], [-1, 1, 0],
])('add(%i, %i) = %i', (a, b, expected) => {
  expect(add(a, b)).toBe(expected);
});

// 3. Always clean up mocks
afterEach(() => { vi.restoreAllMocks(); });

Performance Tips

  • Use happy-dom over jsdom when possible. It is significantly faster for simple DOM operations.
  • Avoid importing heavy modules in test setup files. Use vi.mock() to stub them instead.
  • Use --pool=forks for CPU-intensive tests to isolate them in separate processes.
  • Use --shard in CI to split tests across multiple runners.
  • Minimize beforeEach work. Only set up what each test actually needs.
  • Use .concurrent for tests that do not share mutable state to run them in parallel.

Key Takeaways

  • Vitest reuses your Vite config so there is zero duplicate configuration for transforms, aliases, and plugins.
  • The Jest-compatible API (describe, it, expect, vi) means almost zero learning curve if you already know Jest.
  • Native ESM and TypeScript support eliminates the need for babel-jest or ts-jest transforms.
  • Watch mode uses Vite HMR to re-run only affected tests, making the feedback loop nearly instant.
  • Built-in code coverage via v8 or istanbul with no extra configuration needed.
  • Workspace mode lets you configure different test environments per package in a monorepo.
  • In-source testing lets you co-locate tests inside your source files and tree-shake them out of production builds.
  • vitest bench provides built-in benchmarking with statistical analysis for performance testing.

Conclusion

Vitest has become the de facto testing framework for Vite-based projects and is rapidly gaining adoption across the JavaScript ecosystem. Its key advantage is eliminating the configuration duplication between your build tool and test runner. If you are using Vite for development and production builds, Vitest is the natural choice for testing.

Start with the basics — describe, it, expect — then gradually adopt advanced features like in-source testing, workspace mode, and benchmarking as your project grows. The Jest-compatible API means you can migrate existing tests with minimal changes, often just swapping the import statements.

𝕏 Twitterin LinkedIn
Was this helpful?

Stay Updated

Get weekly dev tips and new tool announcements.

No spam. Unsubscribe anytime.

Try These Related Tools

{ }JSON Formatter.*Regex TesterTSJSON to TypeScript

Related Articles

Playwright E2E Testing Complete Guide

Learn Playwright for end-to-end testing with page objects, fixtures, visual testing, and CI integration.

Software Testing Strategies Guide: Unit, Integration, E2E, TDD & BDD

Complete testing strategies guide covering unit testing, integration testing, E2E testing, TDD, BDD, test pyramids, mocking, coverage, CI pipelines, and performance testing with Jest, Vitest, Playwright, and Cypress.

TypeScript Type Guards: Complete Guide to Runtime Type Checking

Master TypeScript type guards: typeof, instanceof, in operator, custom type guards, discriminated unions, and assertion functions.