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-jestconfiguration, 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()withvi.fn(),vi.mock(),vi.spyOn() - Import from
'vitest'instead of relying on Jest globals, or setglobals: true - Check for any Jest-specific matchers from libraries like
jest-extended— equivalents exist for Vitest but may need separate packages - Remove
ts-jestand 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.