Testing

Vitest: Fast Unit Testing for Modern JS Apps

Mayur Dabhi
Mayur Dabhi
August 20, 2026
13 min read

If your test suite feels slower than your actual development loop, Vitest is probably the fix. Built by the Vite team, Vitest reuses Vite's transform pipeline and dev server architecture to run tests with near-instant startup and hot module reload for your test files. For anyone already on Vite, Next.js, Nuxt, SvelteKit, or any modern bundler-based stack, Vitest has become the default choice for unit and component testing — and it's largely Jest-API-compatible, so migrating existing suites is usually painless.

Why Vitest?

Vitest has surpassed 13,000 GitHub stars and is now the default test runner scaffolded by Vite, Nuxt, and Astro starter templates. Because it shares Vite's config and transform pipeline, there's no separate Babel/webpack setup to maintain — your test environment matches your app environment exactly.

What Is Vitest?

Vitest is a unit test framework powered by Vite. Instead of building its own module resolution and transformation layer (like Jest does with Babel), Vitest delegates that work to Vite — meaning it supports ESM, TypeScript, JSX, and CSS imports out of the box, with zero extra configuration in most projects.

Core features that make Vitest stand out:

Test File *.test.ts *.spec.ts transforms Vite Pipeline esbuild transform ESM module graph dispatches Worker Pool Threads / Forks Parallel execution Reporter / CLI Output

How Vitest runs a test file through Vite's transform pipeline

Installation and Configuration

If your project already uses Vite, adding Vitest takes one command. If not, Vitest works perfectly fine as a standalone test runner too — it will spin up its own minimal Vite instance under the hood.

1

Install Vitest

Add Vitest as a dev dependency. For component testing, also install a DOM environment and the appropriate testing-library package.

Terminal
# Core install
npm install -D vitest

# For DOM-based component testing (React example)
npm install -D jsdom @testing-library/react @testing-library/jest-dom

# Run tests
npx vitest
2

Configure Vitest

If you already have a vite.config.ts, add a test block to it directly. Otherwise create a standalone vitest.config.ts.

vite.config.ts
/// <reference types="vitest" />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

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

Add npm scripts

Wire up convenient commands in package.json for local development and CI.

package.json
{
  "scripts": {
    "test": "vitest",
    "test:run": "vitest run",
    "test:ui": "vitest --ui",
    "test:coverage": "vitest run --coverage"
  }
}
Pro Tip

Setting globals: true lets you use describe, it, and expect without importing them in every file — just like classic Jest. If you'd rather keep things explicit, leave it false and import from vitest instead.

Writing Your First Tests

Vitest's assertion API will feel immediately familiar if you've used Jest, Jasmine, or Chai. Test files are typically named *.test.ts or *.spec.ts and live next to the code they test, or inside a dedicated __tests__ folder.

// 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(-2, -3)).toBe(-5);
  });
});

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

  it('returns a float result', () => {
    expect(divide(10, 4)).toBeCloseTo(2.5);
  });
});
// user-service.test.ts
import { describe, it, expect } from 'vitest';
import { fetchUser } from './user-service';

describe('fetchUser()', () => {
  it('resolves with user data', async () => {
    const user = await fetchUser(1);
    expect(user).toMatchObject({ id: 1, name: expect.any(String) });
  });

  it('rejects for an invalid id', async () => {
    await expect(fetchUser(-1)).rejects.toThrow('User not found');
  });
});
import { beforeEach, afterEach, beforeAll, afterAll, describe, it, expect } from 'vitest';
import { createTestDb, seedDb, clearDb, closeDb } from './test-helpers';

describe('user repository', () => {
  beforeAll(async () => {
    await createTestDb();
  });

  beforeEach(async () => {
    await seedDb();
  });

  afterEach(async () => {
    await clearDb();
  });

  afterAll(async () => {
    await closeDb();
  });

  it('finds a seeded user by email', async () => {
    // test body uses the freshly seeded database
  });
});

Mocking, Spies, and Fake Timers

Vitest's vi object is the equivalent of Jest's global jest object, providing mocking, spying, and timer control without any additional dependencies.

mocking-examples.test.ts
import { describe, it, expect, vi } from 'vitest';
import { sendWelcomeEmail } from './email';
import * as mailer from './mailer';

// Mock an entire module
vi.mock('./mailer');

describe('sendWelcomeEmail()', () => {
  it('calls the mailer with the right arguments', async () => {
    const spy = vi.spyOn(mailer, 'send').mockResolvedValue(true);

    await sendWelcomeEmail('alice@example.com');

    expect(spy).toHaveBeenCalledWith({
      to: 'alice@example.com',
      template: 'welcome',
    });
    expect(spy).toHaveBeenCalledTimes(1);
  });
});

describe('fake timers', () => {
  it('fires a callback after a delay', () => {
    vi.useFakeTimers();
    const callback = vi.fn();

    setTimeout(callback, 1000);
    vi.advanceTimersByTime(1000);

    expect(callback).toHaveBeenCalledOnce();
    vi.useRealTimers();
  });
});
Reset Mocks Between Tests

Mocks persist across tests within the same file unless cleared. Set clearMocks: true or restoreMocks: true in your Vitest config to automatically reset mock state before each test — forgetting this is one of the most common sources of flaky test suites.

Testing UI Components

Vitest pairs naturally with Testing Library for React, Vue, or Svelte components. Because Vitest already runs your components through the same Vite transform your app uses, JSX, TypeScript, and CSS modules just work without extra configuration.

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 the label text', () => {
    render(<Button>Save changes</Button>);
    expect(screen.getByText('Save changes')).toBeInTheDocument();
  });

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

    fireEvent.click(screen.getByRole('button', { name: 'Save' }));

    expect(onClick).toHaveBeenCalledOnce();
  });

  it('is disabled while loading', () => {
    render(<Button loading>Save</Button>);
    expect(screen.getByRole('button')).toBeDisabled();
  });
});
src/test/setup.ts
import '@testing-library/jest-dom/vitest';
import { cleanup } from '@testing-library/react';
import { afterEach } from 'vitest';

// Unmount rendered components after each test to avoid leaks
afterEach(() => {
  cleanup();
});

Coverage Reports and Watch Mode

Vitest ships watch mode by default when you run vitest without arguments — it re-runs only the tests affected by your latest change, using the same dependency graph Vite already builds for your app. For CI, use vitest run for a single non-watching pass.

Terminal
# Watch mode (interactive, reruns on change)
npx vitest

# Single run — use this in CI
npx vitest run

# Generate a coverage report (requires @vitest/coverage-v8)
npx vitest run --coverage

# Open the interactive browser UI
npx vitest --ui

# Run only tests matching a name pattern
npx vitest run -t "user repository"

The V8-based coverage provider requires no source instrumentation step and is fast enough to run on every commit. Install it alongside Vitest:

Terminal
npm install -D @vitest/coverage-v8

Vitest vs Jest: Which Should You Choose?

Jest remains a rock-solid, battle-tested choice — especially for older CRA or plain Node.js projects without Vite in the pipeline. But for any project already built on Vite (or a Vite-based meta-framework), Vitest removes an entire layer of duplicated tooling.

Feature Vitest Jest
Config source Reuses vite.config.ts Separate jest.config.js
Transform engine esbuild (via Vite) Babel / ts-jest
Native ESM support ✅ First-class ⚠️ Requires flags/config
Cold start speed ✅ Very fast ⚠️ Slower (bundling overhead)
Watch mode re-runs Module-graph aware Heuristic file matching
API compatibility Jest-compatible expect/vi Native
Ecosystem maturity Growing fast Extremely mature
Browser-mode testing ✅ Built-in (experimental) ❌ Not built-in

In practice, migrating from Jest to Vitest is often a matter of swapping imports and installing the Vitest packages — most describe/it/expect test bodies need no changes at all.

Conclusion: Key Takeaways

Vitest earns its place as the default test runner for the Vite ecosystem by eliminating the configuration drift between your app's build pipeline and its test pipeline. You get one transform engine, one config file, and a dramatically faster feedback loop — without giving up the Jest-style API most JavaScript developers already know.

Key Takeaways

  • Shared config: Vitest reads your existing vite.config.ts, so app and test environments never drift apart
  • Fast by default: Native ESM and esbuild transforms mean near-instant cold starts and smart, module-graph-aware watch mode
  • Jest-compatible API: describe, it, expect, and the vi mocking object make migration low-friction
  • Built-in coverage: V8-based coverage requires no extra instrumentation step
  • Component-ready: Pairs cleanly with Testing Library for React, Vue, and Svelte components
  • UI mode: An optional visual dashboard makes debugging failing suites faster than scrolling terminal output

If you're starting a new project on Vite, Next.js, Nuxt, or SvelteKit, reach for Vitest first — it's the path of least resistance and, in most cases, the faster test suite too. If you're maintaining a large legacy Jest suite with no Vite in sight, Jest is still a perfectly reasonable choice; just know that the migration path to Vitest exists whenever you're ready for it.

"Vitest isn't trying to reinvent testing — it's trying to make sure your tests run through the exact same pipeline as your app. That consistency is what eliminates an entire class of 'works in tests, breaks in prod' bugs."
Vitest Testing JavaScript Vite TypeScript Unit Testing
Mayur Dabhi

Mayur Dabhi

Full Stack Developer with 5+ years of experience building scalable web applications with Laravel, React, and Node.js.