Vitest took over as the default unit-test runner for Vite-based projects by 2025, and in 2026 it is the obvious choice for any TypeScript/JavaScript project — not just Vite apps. It reuses your existing Vite config, runs tests in native ESM without Babel, and is measurably faster than Jest on medium-to-large test suites. This guide goes from zero to CI-ready coverage in one sitting.
What changed in 2026
- Vitest 2.x stabilized the browser mode and workspace feature, making it possible to test Node code and browser code in one
vitest.config.ts.
- V8 coverage is the default — the Istanbul provider still exists but V8 is faster and requires no instrumentation.
@vitest/ui matured into a proper debugging UI — filterable test tree, inline source, re-run on save.
- Vite 6 perf improvements cut cold-start time significantly; large monorepos feel genuinely fast.
- Jest compat layer still exists but is no longer needed for most projects — Vitest's own APIs are cleaner.
Install
npm install -D vitest @vitest/coverage-v8
# For React component testing add:
npm install -D @testing-library/react @testing-library/jest-dom jsdom
Configure
Add a test block to your existing vite.config.ts — Vitest reads the same file.
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom', // 'node' for back-end code
globals: true, // describe/it/expect without imports
setupFiles: ['./src/test-setup.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'lcov'],
include: ['src/**/*.{ts,tsx}'],
exclude: ['src/**/*.stories.*'],
},
},
})
// src/test-setup.ts
import '@testing-library/jest-dom'
Add scripts to package.json:
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest run --coverage"
}
Write your first test
// src/utils/format.test.ts
import { describe, it, expect } from 'vitest'
import { formatCurrency } from './format'
describe('formatCurrency', () => {
it('formats USD', () => {
expect(formatCurrency(1234.5, 'USD')).toBe('$1,234.50')
})
it('handles zero', () => {
expect(formatCurrency(0, 'USD')).toBe('$0.00')
})
})
Test a React component
// src/components/Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react'
import { Button } from './Button'
it('calls onClick when clicked', () => {
const handler = vi.fn()
render(<Button onClick={handler}>Save</Button>)
fireEvent.click(screen.getByRole('button', { name: /save/i }))
expect(handler).toHaveBeenCalledOnce()
})
The comparison table
| Feature |
Vitest 2.x |
Jest 29 |
| Native ESM |
Yes |
Partial (experimental) |
| TypeScript |
Zero-config |
Needs ts-jest or Babel |
| Cold start (1k tests) |
~1 s |
~5–8 s |
| Watch mode HMR |
Yes (Vite) |
Polling |
| Browser mode |
Built-in |
Separate runner needed |
| Jest API compat |
Yes |
— |
How to pick an environment
| Code under test |
environment setting |
| DOM / React / Vue |
jsdom or happy-dom |
| Node.js / API routes |
node (default) |
| Real browser APIs |
browser (Vitest browser mode) |
Use workspaces when a monorepo has both back-end and front-end packages with different environments.
CI setup
# .github/workflows/test.yml
- name: Run tests
run: npm run test:coverage
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
files: ./coverage/lcov.info
Common mistakes
Forgetting globals: true — without it, every test file needs explicit import { describe, it, expect } from 'vitest'. Add the global once in config.
Mixing jsdom and node — if a test imports a browser-only API in a node environment it will throw at runtime. Match the environment to the code.
Not excluding stories/fixtures from coverage — coverage dips look alarming but are usually untested fixture files. Use the exclude array in coverage config.
Running vitest run in watch mode — vitest run is the one-shot CI mode; plain vitest is watch mode. Don't confuse them in scripts.
What to skip
babel-jest or ts-jest — you don't need a transpilation layer with Vitest's native TypeScript support.
- Separate
jest.config.js when you already have vite.config.ts — one config file is cleaner.
happy-dom by default — jsdom has broader coverage of web APIs; switch to happy-dom only if you have a specific performance problem.
FAQ
Can I migrate from Jest without rewriting tests?
Mostly yes. Vitest's globals: true mode mirrors Jest's API. Mocks (vi.fn(), vi.spyOn(), vi.mock()) map 1-to-1. Edge cases: timer mocks and some module-level mock patterns may need small tweaks.
Does Vitest work outside Vite projects?
Yes. Create a standalone vitest.config.ts without a Vite plugins section and it works for any TypeScript/JavaScript project.
How fast is it really?
On a 500-test React suite cold start is typically under 2 seconds. Incremental re-runs with HMR are near-instant.
What about snapshot testing?
Vitest supports expect(x).toMatchSnapshot() and inline snapshots identically to Jest. Snapshot files live in __snapshots__ by default.
Where to go next