Skip to main content
Ganesh Joshi
Back to Blogs

Vitest for frontend: fast unit testing with Vite

February 15, 20264 min read
Tutorials
Code and tests on screen

Vitest is the natural testing companion for Vite projects. It reuses Vite's transform pipeline, giving you fast test execution with native ESM and TypeScript support. The API mirrors Jest, making migration straightforward. Even outside Vite projects, Vitest offers excellent performance.

Why Vitest?

Feature Jest Vitest
Transform Custom (babel-jest) Vite's esbuild
ES Modules Experimental Native
TypeScript Requires setup Built-in
Watch mode Slow on large projects Instant
Config Separate Shares Vite config

Vitest runs tests in the same environment as your app, eliminating configuration mismatches.

Installation

For Vite projects:

npm install -D vitest @vitest/ui jsdom

For React projects, add Testing Library:

npm install -D @testing-library/react @testing-library/jest-dom

Configuration

Extend your Vite config:

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: ['./src/test/setup.ts'],
    include: ['src/**/*.{test,spec}.{js,ts,jsx,tsx}'],
  },
});

Or create a separate config:

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

export default defineConfig({
  test: {
    globals: true,
    environment: 'jsdom',
  },
});

Setup file

Create a setup file for global configuration:

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

// Cleanup after each test
afterEach(() => {
  cleanup();
});

Package.json scripts

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

Writing tests

Basic test

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

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

  it('multiplies two numbers', () => {
    expect(multiply(2, 3)).toBe(6);
  });
});

React component test

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

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

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

    fireEvent.click(screen.getByRole('button'));

    expect(handleClick).toHaveBeenCalledTimes(1);
  });

  it('is disabled when disabled prop is true', () => {
    render(<Button disabled>Click</Button>);
    expect(screen.getByRole('button')).toBeDisabled();
  });
});

Async tests

import { describe, it, expect } from 'vitest';
import { fetchUser } from './api';

describe('fetchUser', () => {
  it('returns user data', async () => {
    const user = await fetchUser('123');
    expect(user).toEqual({
      id: '123',
      name: 'Alice',
    });
  });

  it('throws on invalid id', async () => {
    await expect(fetchUser('')).rejects.toThrow('Invalid ID');
  });
});

Mocking

Mock functions

import { vi } from 'vitest';

const mockFn = vi.fn();
mockFn('arg');
expect(mockFn).toHaveBeenCalledWith('arg');

Mock modules

import { vi } from 'vitest';

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

Mock timers

import { vi, beforeEach, afterEach } from 'vitest';

beforeEach(() => {
  vi.useFakeTimers();
});

afterEach(() => {
  vi.useRealTimers();
});

it('delays execution', () => {
  const callback = vi.fn();
  setTimeout(callback, 1000);

  vi.advanceTimersByTime(1000);

  expect(callback).toHaveBeenCalled();
});

Snapshot testing

it('matches snapshot', () => {
  const { container } = render(<Button>Click</Button>);
  expect(container).toMatchSnapshot();
});

// Inline snapshot
it('matches inline snapshot', () => {
  expect(formatDate('2026-02-23')).toMatchInlineSnapshot(`"Feb 23, 2026"`);
});

Code coverage

Install coverage provider:

npm install -D @vitest/coverage-v8

Configure:

// vite.config.ts
test: {
  coverage: {
    provider: 'v8',
    reporter: ['text', 'html'],
    exclude: ['node_modules/', 'src/test/'],
    thresholds: {
      lines: 80,
      functions: 80,
      branches: 80,
      statements: 80,
    },
  },
}

Run:

npm run test:coverage

UI mode

The Vitest UI provides a visual test runner:

npm run test:ui

Features:

  • Visual test tree
  • Filter by status
  • Watch mode controls
  • Module graph view

Watch mode

By default, vitest runs in watch mode:

npm run test
  • Re-runs affected tests on file change
  • Press a to run all tests
  • Press f to run failed tests
  • Press p to filter by file pattern
  • Press q to quit

Testing hooks

import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';

describe('useCounter', () => {
  it('increments counter', () => {
    const { result } = renderHook(() => useCounter());

    act(() => {
      result.current.increment();
    });

    expect(result.current.count).toBe(1);
  });
});

Concurrent tests

Run tests in parallel:

describe.concurrent('parallel tests', () => {
  it('test 1', async () => { /* ... */ });
  it('test 2', async () => { /* ... */ });
});

Summary

Vitest provides fast, native ESM testing with a Jest-compatible API. Configure in vite.config.ts for Vite projects or standalone. Use Testing Library for React components. Mock with vi.fn() and vi.mock(). Enable coverage with @vitest/coverage-v8. Run vitest --ui for a visual interface. Watch mode makes TDD workflows smooth.

Frequently Asked Questions

Vitest is a fast unit test runner built for the Vite ecosystem. It uses Vite's transform pipeline for speed, supports ESM natively, and has a Jest-compatible API for easy migration.

Vitest uses Vite for transforms (faster, native ESM), shares your Vite config, and has instant watch mode. Jest uses its own transform pipeline and was designed for CommonJS. The API is nearly identical.

Yes. Vitest can run standalone with its own config. You get the same fast execution and ESM support. Configure it in vitest.config.ts instead of extending vite.config.ts.

Install @testing-library/react and @testing-library/jest-dom. Configure jsdom as the environment. Import render and screen, then write tests using familiar Testing Library patterns.

Yes. Run vitest --coverage to generate coverage reports. Install @vitest/coverage-v8 or @vitest/coverage-istanbul for the coverage provider. Configure thresholds in vitest.config.ts.

Related Posts