Vitest: Fast Unit Testing for Modern JS Apps
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.
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:
- Instant startup: Vite's native ESM dev server means no bundling step before tests run
- Smart watch mode: Only re-runs tests affected by changed files, using the same module graph as your app
- Jest-compatible API:
describe,it,expect, and most matchers work exactly as they do in Jest - Built-in mocking:
vi.fn(),vi.spyOn(), and module mocking without extra packages - Native TypeScript & JSX: No
ts-jestor Babel plugin juggling required - In-source testing: Colocate tests directly inside implementation files if you prefer
- UI mode: An optional browser-based dashboard for visually inspecting test runs
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.
Install Vitest
Add Vitest as a dev dependency. For component testing, also install a DOM environment and the appropriate testing-library package.
# 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
Configure Vitest
If you already have a vite.config.ts, add a test block to it directly. Otherwise create a standalone vitest.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'],
},
},
});
Add npm scripts
Wire up convenient commands in package.json for local development and CI.
{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"test:ui": "vitest --ui",
"test:coverage": "vitest run --coverage"
}
}
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.
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();
});
});
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.
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();
});
});
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.
# 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:
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 thevimocking 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."