TL;DR:

  • Vitest is a fast unit testing framework for JavaScript and TypeScript, built on Vite — compatible with Jest’s API but significantly faster to start up and run
  • Native TypeScript and ESM support means no babel transforms, no ts-jest configuration, no teardown of import syntax at runtime
  • If you’re already using Vite, Vitest reuses your existing config; if you’re not, it works fine as a standalone test runner

Jest has been the default JavaScript test runner for years. It works, it’s stable, and most developers know it. The problems show up as projects grow: slow startup, TypeScript requiring either ts-jest or Babel transforms, ESM support that ranges from awkward to broken, and test isolation that makes debugging harder than it should be.

Vitest solves most of these. It uses Vite under the hood, which means native ESM and TypeScript support without transformation layers, and startup times that are typically 3-10x faster than Jest on equivalent test suites.

Installation

If you’re using Vite already:

pnpm add -D vitest

Add a test script to package.json:

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

vitest runs in watch mode by default (re-runs tests on file save). vitest run runs once and exits — what you want in CI.

If you’re not using Vite, add a minimal Vitest config:

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

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

Writing Tests

The API is Jest-compatible. If you’ve written Jest tests, you already know Vitest:

import { describe, it, expect, beforeEach } from 'vitest';
import { UserService } from '../services/user';

describe('UserService', () => {
  let service: UserService;

  beforeEach(() => {
    service = new UserService();
  });

  it('creates a user with a hashed password', async () => {
    const user = await service.create({ email: 'test@example.com', password: 'secret' });
    expect(user.email).toBe('test@example.com');
    expect(user.password).not.toBe('secret');
    expect(user.password).toHaveLength(60); // bcrypt hash
  });

  it('throws when email already exists', async () => {
    await service.create({ email: 'existing@example.com', password: 'pass' });
    await expect(
      service.create({ email: 'existing@example.com', password: 'other' })
    ).rejects.toThrow('Email already in use');
  });
});

You can use globals (no imports needed for describe, it, expect) if you set globals: true in config. The downside is that TypeScript won’t know the globals exist without a tsconfig include for vitest/globals — add it to your tsconfig’s types array.

Mocking

Vitest uses vi where Jest uses jest:

import { vi, describe, it, expect } from 'vitest';
import { sendEmail } from '../email';
import { UserService } from '../services/user';

vi.mock('../email', () => ({
  sendEmail: vi.fn().mockResolvedValue({ messageId: 'test-123' }),
}));

describe('UserService.register', () => {
  it('sends a welcome email after registration', async () => {
    const service = new UserService();
    await service.register({ email: 'new@example.com', password: 'pass' });
    
    expect(sendEmail).toHaveBeenCalledOnce();
    expect(sendEmail).toHaveBeenCalledWith(
      expect.objectContaining({ to: 'new@example.com' })
    );
  });
});

Module mocking in Vitest is hoisted to the top of the file, same as Jest. Spy functions work the same way — vi.spyOn(object, 'method').

Testing in a Browser Environment

By default Vitest runs tests in Node. For code that touches the DOM, set environment to jsdom or happy-dom:

pnpm add -D @vitest/ui happy-dom
// vitest.config.ts
export default defineConfig({
  test: {
    environment: 'happy-dom', // or 'jsdom'
  },
});

You can also set the environment per file with a comment at the top:

// @vitest-environment happy-dom
import { it, expect } from 'vitest';
import { render } from '@testing-library/vue';
import MyComponent from './MyComponent.vue';

it('renders the title', () => {
  const { getByText } = render(MyComponent, { props: { title: 'Hello' } });
  expect(getByText('Hello')).toBeTruthy();
});

This is useful if most of your tests are Node-based but a handful test DOM components — you don’t need a separate config file.

Snapshot Testing

Vitest supports snapshots with the same API as Jest:

it('renders the nav menu correctly', () => {
  const { container } = render(NavMenu, { props: { items: mockItems } });
  expect(container.innerHTML).toMatchSnapshot();
});

Snapshots are stored in __snapshots__ directories adjacent to your test files, same as Jest. Update snapshots with vitest -u.

Code Coverage

pnpm add -D @vitest/coverage-v8
// vitest.config.ts
export default defineConfig({
  test: {
    coverage: {
      provider: 'v8',
      reporter: ['text', 'lcov'],
      exclude: ['node_modules', 'dist', '**/*.config.*'],
    },
  },
});

Run with vitest run --coverage. The lcov reporter generates output that GitHub Actions, Codecov, and most CI coverage tools can consume.

Running in CI

- name: Test
  run: pnpm test:run --reporter=verbose

- name: Test with coverage
  run: pnpm vitest run --coverage

Vitest exits with a non-zero code when tests fail, as you’d expect. The --reporter=verbose flag prints individual test names rather than just a summary, which is useful in CI logs.

The Vitest UI

Vitest ships an optional browser UI for exploring test results:

pnpm vitest --ui

It opens a browser interface showing test files, results, and the ability to filter and re-run individual tests. Useful for local debugging; you wouldn’t use this in CI.

Migrating from Jest

If you have an existing Jest suite, migration is mostly mechanical. The main changes:

  • Replace jest.fn(), jest.mock(), jest.spyOn() with vi.fn(), vi.mock(), vi.spyOn()
  • Import from 'vitest' instead of relying on Jest globals, or set globals: true
  • Check for any Jest-specific matchers from libraries like jest-extended — equivalents exist for Vitest but may need separate packages
  • Remove ts-jest and Babel transform config if you had them

Most projects migrate in a few hours. Tests that rely on Jest’s module registry or jest.requireActual have more straightforward Vitest equivalents that you can work through mechanically.

If you’re starting a new project in 2026, Vitest is the default choice for unit testing in the Vite ecosystem. For projects not using Vite, it’s still worth evaluating — the improved TypeScript experience and startup performance are real advantages.