vitest review
Vitest runs JavaScript and TypeScript tests through Vite's transform and module-resolution pipeline. That makes it a close fit for projects whose source already depends on Vite aliases, plugins, JSX, or ESM. Its test, expect, snapshot, spy, and module-mocking APIs will look familiar to Jest users, while projects, sharding, type tests, and an optional browser mode cover larger suites. Version 4.1.11 fixes lifecycle concurrency limits, browser iframe URL handling, Chromium cleanup on low disk space, and the file-system boundary used for mock redirects. In our Node 22 sandbox, ESM import worked and require() failed.
Vitest 4.1.11 is a strong match for Vite and ESM projects that will use its shared transform pipeline. Keep it out of require-only tools, and budget separate packages for DOM, browser, UI, or coverage work.
We installed it
| Install | ✓ · 9.1s | 47 packages on disk · 39 MB |
| Import | ½ | ESM import works · require() fails · ESM package with exports map |
| Browser | 94.4 KB | gzipped (306 KB minified), bundled with esbuild |
| Types | ✓ | TypeScript types bundled |
| Known vulns | 0 | 0 critical · 0 high · 0 moderate · 0 low (npm audit) |
Answers from our run
Does vitest install cleanly?
Yes. In a fresh container with an empty cache, npm install vitest finished in 9 seconds, leaving 47 packages and 39 MB on disk. npm audit reported no known vulnerabilities.
How much does vitest add to a browser bundle?
94.4 KB gzipped (306 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.
Does vitest work with both ESM and CommonJS?
ESM only. import 'vitest' worked, require('vitest') failed in our run, so CommonJS projects need a dynamic import or a build step.
Does vitest include TypeScript types?
Yes, type declarations ship inside the package, so no @types install is needed.
vitest or jest: which should you use?
jest: Choose it for an established Jest suite that depends on its custom runners, transforms, or CommonJS-era behavior. Vitest 4.1.11 is a strong match for Vite and ESM projects that will use its shared transform pipeline.
When should you not use vitest?
Your runner must load through require(): our CommonJS load test failed on Node.js 22.23.2 even though the package exposes compatibility entries
Discussed on
- hnVitest Browser Mode Guide72 points
- hnJest/Vitest interactive course (runs in the browser)52 points
- hnVitest vs. Jest44 points
- hnVitest, fast unit-test framework powered by Vite25 points
- hnGetting Started with Vitest20 points
Use it if
- Your application already uses Vite and its tests need the same aliases, transforms, plugins, and TypeScript handling
- You want Jest-shaped assertions, snapshots, spies, and mocks in an ESM-first codebase
- Your monorepo needs named test projects with different environments, setup files, or worker settings
- You need to split a large suite across CI machines with shards and merge their reports afterward
- Your runner must load through require(): our CommonJS load test failed on Node.js 22.23.2 even though the package exposes compatibility entries
- A small Node library only needs assertions and test discovery: node:test is already in Node and avoids Vitest's 47-package, 39 MB installation from our sandbox
- Your CI is pinned outside the declared Node range of ^20.0.0, ^22.0.0, or >=24.0.0; installation on that runtime is unsupported
- You expect a DOM, real browser, UI, or coverage engine in the base package: jsdom, browser providers, @vitest/ui, and coverage providers are optional peer installs
- Your suite relies on require-based module interception: the v4.1.11 API docs say vi.mock only works on modules loaded with import
Setup reality
Our clean Node 22 install completed in 9.1 seconds and left 47 packages using 39 MB on disk. npm audit found 0 known vulnerabilities. Vitest declares 20 direct dependencies and 12 peer dependencies; its published archive is 2216 KB unpacked and includes TypeScript declarations. ESM import worked. require() failed under Node.js 22.23.2. A browser-oriented esbuild bundle measured 306 KB minified and 94.4 KB gzipped, so this is tooling to keep out of application code.
A basic vitest.config.ts imports defineConfig from vitest/config. The default environment is node, and global test functions are off unless you enable globals. DOM tests require jsdom or happy-dom, while real-browser runs need a browser provider. Setup files execute inside test workers before each file; globalSetup runs once in the main process. If isolation is disabled, imported modules stay cached even though setup files execute again.
Coverage is another install: choose @vitest/coverage-v8 or @vitest/coverage-istanbul, then run vitest run --coverage. The runner can offer to install the selected provider on first use, which is an awkward surprise in a locked CI job. V8 coverage needs a V8-based runtime and cannot cover Firefox, Bun, or Cloudflare Workers. Istanbul works across more runtimes but instruments source before execution. Set coverage.include if untouched files must appear in the report.
Test files run in parallel workers and isolated environments by default. Concurrent tests use Promise.all and obey maxConcurrency; version 4.1.11 fixes that limit for lifecycle hooks. The default forks pool is the safer choice for native Node add-ons. The docs warn that threads can trigger native crashes and a Node fetch error. vi.mock is hoisted, only sees import-loaded modules, and cannot capture ordinary local variables; use vi.hoisted when a mock factory needs shared state.
Patterns
Write a basic unit test write-unit-test
import { describe, expect, test } from 'vitest'
describe('subtotal', () => {
test('adds line items', () => {
expect(subtotal([4, 7])).toBe(11)
})
})These functions are explicit imports by default. Enable globals only if the shorter syntax is worth adding vitest/globals to your TypeScript types.
Configure files and cleanup configure-test-runner
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
include: ['src/**/*.test.ts'],
setupFiles: ['./test/setup.ts'],
restoreMocks: true,
},
})setupFiles run inside each test worker. Use globalSetup for one-time services that must start before workers are created.
Assert promise results test-async-result
import { expect, test } from 'vitest'
test('loads one account', async () => {
await expect(loadAccount('a1')).resolves.toMatchObject({ id: 'a1' })
await expect(loadAccount('missing')).rejects.toThrow('not found')
})Await the resolves or rejects chain. Otherwise the test can finish before the assertion settles.
Record calls with vi.fn mock-function
import { expect, test, vi } from 'vitest'
test('publishes once', async () => {
const publish = vi.fn().mockResolvedValue({ id: 'm1' })
await saveOrder({ publish })
expect(publish).toHaveBeenCalledOnce()
expect(publish).toHaveBeenCalledWith('order.saved')
})Enable clearMocks, mockReset, or restoreMocks deliberately. Each option cleans a different part of mock state.
Replace an imported module mock-es-module
import { expect, test, vi } from 'vitest'
import { readRate } from './rates.js'
vi.mock('./rates.js', () => ({
readRate: vi.fn().mockResolvedValue(1.25),
}))
test('uses the fixed rate', async () => {
await expect(readRate('USD')).resolves.toBe(1.25)
})vi.mock is hoisted and only affects modules loaded through import. It does not intercept require calls.
Give a mock factory shared state share-hoisted-mock-state
import { expect, test, vi } from 'vitest'
const mocks = vi.hoisted(() => ({ fetchUser: vi.fn() }))
vi.mock('./users.js', () => ({ fetchUser: mocks.fetchUser }))
test('handles a missing user', async () => {
mocks.fetchUser.mockResolvedValue(null)
await expect(showUser('x')).resolves.toBe('missing')
})Ordinary top-level variables are unavailable to a hoisted vi.mock factory. Create the shared value with vi.hoisted.
Advance a delayed action control-fake-time
import { afterEach, expect, test, vi } from 'vitest'
afterEach(() => vi.useRealTimers())
test('flushes after 500 ms', async () => {
vi.useFakeTimers()
const flush = vi.fn()
scheduleFlush(flush)
await vi.advanceTimersByTimeAsync(500)
expect(flush).toHaveBeenCalledOnce()
})Return to real timers after every test. Async timer advancement is safer when callbacks create promises.
Select jsdom for one file test-dom-code
/**
* @vitest-environment jsdom
*/
import { expect, test } from 'vitest'
test('renders the status', () => {
document.body.innerHTML = '<p data-status>ready</p>'
expect(document.querySelector('[data-status]')?.textContent).toBe('ready')
})Install jsdom separately. A file comment is useful when most of the suite should stay in the faster node environment.
Enforce a coverage floor collect-coverage
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
coverage: {
provider: 'v8',
include: ['src/**/*.ts'],
thresholds: { lines: 85, branches: 80 },
},
},
})Install @vitest/coverage-v8 at the same version as Vitest, then run vitest run --coverage. include is needed if untouched source files must count.
Run Node and DOM projects split-monorepo-projects
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
projects: [
{ test: { name: 'server', include: ['server/**/*.test.ts'] } },
{ test: { name: 'client', include: ['client/**/*.test.ts'], environment: 'jsdom' } },
],
},
})Project names should be unique because reporters and CLI filtering use them to identify results. The client project still needs jsdom installed.
Bound concurrent work limit-concurrent-tests
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
maxConcurrency: 4,
},
})
// In a test file:
test.concurrent('reads account', async ({ expect }) => {
expect(await readAccount()).toBeTruthy()
})Concurrent tests run through Promise.all. Use the expect supplied by the test context so snapshots and assertions attach to the correct test.
Split tests across CI jobs shard-ci-suite
# job 1
npx vitest run --shard=1/3 --reporter=blob
# job 2
npx vitest run --shard=2/3 --reporter=blob
# job 3
npx vitest run --shard=3/3 --reporter=blobUpload every blob report and merge them in a later job. Each shard must use the same Vitest configuration and dependency versions.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| jest | npm | Choose it for an established Jest suite that depends on its custom runners, transforms, or CommonJS-era behavior. |
| mocha | npm | Choose it when you want a smaller, assertion-agnostic Node test runner and prefer assembling the surrounding tools yourself. |
| uvu | npm | Choose it for a compact Node suite where single-threaded execution and a narrow API are acceptable. |
| @web/test-runner | npm | Choose it when standards-based browser execution matters more than sharing a Vite-centered unit-test stack. |
More testing guides
pytest · chai · jsdom · playwright · coverage · axe-core · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

