Browse the Knowledge Hub211 resources
Vitest review: what it is, pros, cons and setup
Vitest is a test framework that runs on Vite’s module pipeline, so TypeScript, JSX and ES modules work through the same configuration and plugins as the application. It offers a largely Jest-compatible API with built-in mocking, snapshots and coverage, plus a Browser Mode that runs tests in real browsers. Vitest 5 was released on 3 September 2026 and is free under the MIT licence.
QAble has evaluated this tool and works alongside it. The factual sections are complete; our view is bounded to what we have actually seen rather than a long-term verdict.
- last reviewed
- 13 October 2026
- version covered
- Vitest 5
- licence
- Open source
At a glance
Vitest at a glance
- Vendor
- Vitest project
- Category
- Unit testing
- Licence
- Open source (MIT)
- Platforms
- Node.js, Browser
- Languages
- JavaScript, TypeScript
- Integrates with
- Vite, Testing Library, Playwright
- First released
- 2021
- Version reviewed
- Vitest 5
Verdict
Our verdict on Vitest
Strengths
- Reuses vite.config, so aliases, plugins and TypeScript handling match the application with no separate transform setup
- TypeScript and native ES modules run without Babel or ts-jest
- Jest-style API: describe, test, expect, snapshots and vi in place of jest
- Browser Mode runs the same tests in real Chromium, Firefox or WebKit through Playwright
- Sharding with blob reports and merge-reports is built in, with no paid service
- Free under MIT, with VoidZero committing to keep it MIT-licensed
Weaknesses
- Vitest 5 changed defaults and removed APIs, and many guides still describe version 3 or 4
- Jest compatibility is close but not complete; mock resets and module mocks behave differently
- Globals are off by default, which breaks Jest-style tests and Testing Library auto-cleanup
- Requires Node 22.12 or later and Vite 6.4 or later as a peer dependency
Our take
Vitest is the natural choice for any project already built with Vite, and a strong one for TypeScript or ESM-first codebases generally, because there is no separate transform setup to keep in step with the build. QAble has evaluated it rather than maintained a long-running suite on it, so treat this as a bounded take. Version 5 is only a little over a month old and tightens several behaviours at once: clearMocks is on by default, vi.mock calls must be at the top level, unawaited async assertions fail the test and benchmarks moved to a new API. For an existing suite, run the migration guide on a branch before upgrading. When migrating from Jest, the API similarity hides real differences in mock reset behaviour, module mocking and globals, so budget for test fixes rather than a find-and-replace.
We have evaluated this tool rather than maintained a long-running suite on it, so this is a bounded view rather than a long-term verdict. The rest of this page is factual.
How it works
What is Vitest and how does it work?
Vite’s pipeline does the transforming
Vitest starts a Vite server in its main process and loads every test file and its imports through Vite’s module runner, which applies the same plugins, aliases and transforms as the application build. TypeScript, JSX, CSS imports and framework single-file components therefore work without a second configuration, and changes to vite.config apply to tests as well. A separate vitest.config file can extend or override it. Because transformed modules are cached by the server, watch mode re-runs only the tests affected by a change, using Vite’s module graph to work out which ones.
Pools of workers, isolated by file
Test files run in a pool of workers. The default pool is forks, child processes, chosen for stability; threads, vmThreads and vmForks are alternatives that trade isolation or compatibility for speed. Each file is isolated by default, so module state does not leak between files, and tests within a file run one after another unless marked concurrent. On large machines the single Vite server in the main process can become the bottleneck, which is why the documentation suggests sharding across processes even on one machine.
Three ways to provide a DOM
The environment option chooses node, jsdom or happy-dom for each test file or project, which suits logic and simple component tests. Browser Mode goes further: tests run inside a real browser driven by a provider package, @vitest/browser-playwright or the community-maintained @vitest/browser-webdriverio, with locators and interactions sent through the provider. That gives real layout, events and rendering, and since 4.0 the mode is stable. Version 5 adds a trace view that records each interaction as a DOM snapshot to step through after a failure.
Mocking through module interception
vi.mock replaces a module for the test file. Vitest hoists vi.mock calls above the imports, so the mock is in place before the code under test loads it, and in version 5 those calls must be at the top level of the file or Vitest throws. vi.fn and vi.spyOn create mock functions, vi.mocked adds types, and vi.when, new in version 5, sets return values per argument list. Unlike Jest, there is no automatic loading of __mocks__ folders without a vi.mock call, and importing the real module inside a factory is asynchronous, through vi.importActual.
Strengths
What it does well
One configuration for app and tests
Aliases, environment variables, plugins and TypeScript paths come from the Vite configuration, so a test imports modules exactly as the application does. The class of failure where tests pass with different resolution rules from the build largely disappears.
Projects for mixed environments
The projects option runs node, jsdom and browser tests from one command with one report. In version 5, inline projects inherit the root configuration by default, nested projects are supported for monorepos, and -p selects a project by name.
Browser Mode with visual checks
Browser Mode runs component and integration tests in real browsers with an interaction API and locators, which are strict by default in version 5. toMatchScreenshot adds visual regression comparisons, and the version 5 trace view replays failed tests step by step without a separate viewer.
Type tests alongside runtime tests
expectTypeOf and assertType in *.test-d.ts files check types through tsc or vue-tsc as part of the test run. Library authors can assert that a public API infers the types they promise, and a regression fails the build like any other test.
Sharding and merged reports
--shard splits test files across machines, the blob reporter stores each shard’s results in .vitest/blob, and --merge-reports combines them into one report. Since 4.1 the GitHub Actions reporter also writes a job summary.
Tools for finding slow and flaky tests
The duration breakdown shows how much time goes to environment setup, imports, transforms and tests. Version 5 prints performance hints by default, adds vitest doctor to try alternative configurations, and adds --repeats to run each test several times when hunting flakiness.
Pricing
Vitest pricing and licensing
Vitest is free and open source under the MIT licence, copyright VoidZero Inc. and contributors. VoidZero, which employs core maintainers, joined Cloudflare on 4 June 2026 and stated that Vite, Vitest and its other projects will remain open source and MIT-licensed. There is no paid edition and no feature held back. Vite+, VoidZero’s bundled toolchain that runs Vitest through vp test, reached 1.0 on 28 September 2026 and is also free under MIT. Costs are CI time and upgrade effort.
- Licence: MIT for Vitest and its official packages, stated in the vitest-dev/vitest repository.
- No tiers: Browser Mode, coverage, sharding, the UI and the HTML reporter are all in the open-source packages on npm.
- Funding: core maintainers work on Vite and Vitest at VoidZero, with further support from sponsors through GitHub Sponsors and Open Collective.
- Infrastructure: CI minutes scale with suite size; Browser Mode adds the cost of installing and running real browsers on runners.
- Hidden cost: majors. Version 4 removed several configuration options and version 5 changed defaults, so each major needs a planned migration.
Checked against the vendor’s own pages on 13 October 2026. Pricing changes; confirm on the official site before buying.
Getting started
Vitest tutorial: getting started
Setup, step by step
- Use Node.js 22.12 or later.
- Install Vitest as a dev dependency; Yarn users add vite explicitly, since it is now a peer dependency.
- Add .vitest/ to .gitignore; reporters and artifacts write there in version 5.
- Create a file whose name contains .test. or .spec., such as cart.test.ts.
- Import describe, test and expect from vitest, since globals are off by default.
- Add "test": "vitest" to package.json scripts, and run npm test for watch mode or npx vitest run once.
Install
npm install -D vitest
# watch mode while developing
npx vitest
# single run, as CI does
npx vitest runA first example
// src/cart.test.ts
import { describe, expect, test } from 'vitest'
import { Cart } from './cart'
describe('Cart', () => {
test('sums line items in pence', () => {
const cart = new Cart()
cart.add('SKU-1', { quantity: 2, unitPence: 1500 })
cart.add('SKU-2', { quantity: 1, unitPence: 499 })
expect(cart.totalPence()).toBe(3499)
})
test('rejects a zero quantity', () => {
const cart = new Cart()
expect(() => cart.add('SKU-1', { quantity: 0, unitPence: 1500 }))
.toThrow('quantity must be positive')
})
test('fails to load a missing cart', async () => {
// Vitest 5 fails the test if this assertion is not awaited
await expect(Cart.load('missing-id')).rejects.toThrow('not found')
})
})What trips people up first
ReferenceError: describe is not defined, or expect is not defined, usually in tests copied from Jest. Vitest does not inject globals unless asked. Either import describe, test, expect and vi from vitest in each file, which is the documented default, or set globals: true in the test configuration and add vitest/globals to the TypeScript types. The same setting affects Testing Library: without globals, its automatic cleanup after each test does not run, so components from one test remain in the DOM for the next. Import cleanup and call it in afterEach, or enable globals.
In practice
Using Vitest in practice
Configuring projects and coverage
Keep test settings in vitest.config.ts, using defineConfig from vitest/config, or in the test block of vite.config. Split suites by environment with projects: fast unit tests in node, component tests in jsdom or the browser. Coverage is configured at the root and, since version 4, reports only files that tests load unless you set coverage.include, so set it to your source folder or untested files vanish from the report. The V8 provider is the default choice and since version 4 remaps coverage through the AST, which the release post describes as matching Istanbul’s accuracy.
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import { playwright } from '@vitest/browser-playwright'
export default defineConfig({
test: {
coverage: {
provider: 'v8',
include: ['src/**/*.{ts,tsx}'], // otherwise untested files are not reported
},
projects: [
{
// Inline projects inherit the root config by default in Vitest 5
test: { name: 'unit', environment: 'node', include: ['src/**/*.unit.test.ts'] },
},
{
test: {
name: 'browser',
include: ['src/**/*.browser.test.tsx'],
browser: {
enabled: true,
provider: playwright(),
instances: [{ browser: 'chromium' }],
},
},
},
],
},
})Mocking modules the Vitest way
Put vi.mock at the top level of the file; Vitest hoists it above imports, and version 5 throws if it is nested inside a test or a function. A factory must return the module’s exports explicitly, including default if the module has one. To keep part of the real module, await vi.importActual inside the factory. Use vi.mocked for typed access to mocked functions, and vi.when for per-argument behaviour instead of branching implementations. clearMocks now defaults to true, so call history resets before every test while implementations stay in place.
// src/orders.test.ts
import { expect, test, vi } from 'vitest'
import { placeOrder } from './orders'
import { charge } from './payment-client'
// Hoisted above the imports; must stay at the top level in Vitest 5
vi.mock('./payment-client', async () => {
const actual = await vi.importActual<typeof import('./payment-client')>('./payment-client')
return { ...actual, charge: vi.fn() }
})
test('charges the card once and marks the order paid', async () => {
vi.mocked(charge).mockResolvedValue({ id: 'ch_1', status: 'succeeded' })
const order = await placeOrder({ totalPence: 4200, card: 'tok_visa' })
expect(charge).toHaveBeenCalledTimes(1)
expect(charge).toHaveBeenCalledWith(4200, 'tok_visa')
expect(order.status).toBe('paid')
})Sharding in GitHub Actions
The documented pattern is a matrix of shard jobs, each running with the blob reporter, followed by one job that downloads every blob and merges them. Blobs land in .vitest/blob in version 5, and the upload step must include hidden files because the folder name starts with a dot. Shards split test files, not individual tests, so keep file sizes roughly even. The GitHub Actions reporter is added automatically when Vitest detects it is running in Actions, so failures are annotated on the pull request without extra configuration.
# .github/workflows/test.yml
name: test
on: [push, pull_request]
jobs:
vitest:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
- run: npm ci
- run: npx vitest run --reporter=blob --shard=${{ matrix.shard }}/${{ strategy.job-total }}
- uses: actions/upload-artifact@v7
if: ${{ !cancelled() }}
with:
name: blob-${{ matrix.shard }}
path: .vitest
include-hidden-files: true
merge:
if: ${{ !cancelled() }}
needs: vitest
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
- run: npm ci
- uses: actions/download-artifact@v8
with:
path: .vitest
merge-multiple: true
- run: npx vitest run --merge-reportsMigrating a Jest suite
Start by replacing jest with vi and importing from vitest, or set globals: true to keep the global style. Then work through the differences the official migration guide lists. mockReset restores the original implementation in Vitest, where Jest leaves an empty function. Module mock factories must return explicit exports, and __mocks__ folders load only when vi.mock is called. jest.requireActual becomes an awaited vi.importActual. The done callback is not supported, so convert callback tests to async functions. Test names are joined with a greater-than sign, which changes -t patterns, and JEST_WORKER_ID becomes VITEST_POOL_ID. Snapshot headers differ, so regenerate snapshots once and review the diff.
Upgrading to Vitest 5
Upgrade Node to 22.12 or later and Vite to 6.4 or later first. Then expect four kinds of failure. Tests that relied on call history carrying between tests break because clearMocks defaults to true. vi.mock calls inside functions now throw. Unawaited resolves, rejects and toMatchFileSnapshot assertions fail instead of warning, and expect.poll rejects on timeout. Benchmarks need rewriting, because bench is now a fixture inside test rather than a top-level import. Also check custom matcher typings, which take the return type as their first parameter, and any tooling that read reports from the old locations now that everything is written under .vitest. Teams on version 3 should read the version 4 migration guide first.
Pitfalls
Common pitfalls
Copying Jest tests that rely on globals
Why it happens: Vitest does not inject describe, test, expect or vi unless globals is enabled, so copied tests fail with reference errors and Testing Library stops cleaning up between tests.
The fix: Import the functions from vitest, or set globals: true and add vitest/globals to tsconfig types. Without globals, call Testing Library’s cleanup in afterEach.
vi.mock inside a test or helper
Why it happens: Vitest hoists vi.mock to the top of the file. Placed inside a function it never applied as written, and since version 5 Vitest throws instead of guessing.
The fix: Keep vi.mock at the top level, use vi.hoisted for values the factory needs, and use vi.doMock for the rare case where a mock must be applied later.
Assuming mockReset matches Jest
Why it happens: In Vitest, mockReset restores the original implementation passed to vi.fn; in Jest it leaves a function that returns undefined. Migrated tests then call real code unexpectedly.
The fix: Read the Migrating from Jest guide before converting, set explicit implementations in beforeEach, and rely on the clearMocks default for call history.
Coverage that ignores untested files
Why it happens: Version 4 removed coverage.all, so by default only files that some test imports appear in the report. A module with no tests at all is simply absent, and the percentage looks better than it is.
The fix: Set coverage.include to the source folders that should be measured, so untested files appear with zero coverage.
Unawaited async assertions
Why it happens: Older versions awaited a forgotten expect(...).resolves at the end of the test and printed a warning. Version 5 fails the test, so suites that upgraded without fixing the warnings go red.
The fix: Await every resolves, rejects and toMatchFileSnapshot assertion. Lint rules for floating promises catch these before they reach CI.
Following configuration examples from version 3 or earlier
Why it happens: workspace files, poolMatchGlobs, environmentMatchGlobs, poolOptions, maxThreads, the basic reporter and deps.inline were removed or renamed in version 4, and many articles still show them.
The fix: Use projects instead of workspace and match globs, maxWorkers instead of maxThreads and maxForks, server.deps.inline instead of deps.inline, and check the migration guide for each removed option.
Compared
How Vitest compares
| Tool | Included | Code transformation | Native ES modules | Real browser runs | Licence |
|---|---|---|---|---|---|
| Vitest | Runner, assertions, mocks, snapshots, coverage | Vite pipeline and plugins | Yes | Browser Mode via Playwright or WebdriverIO | MIT |
| Jest | Runner, assertions, mocks, snapshots, coverage | Babel by default, or ts-jest | Experimental, behind a Node flag | No; jsdom simulation | MIT |
| Mocha | Runner only; add Chai, Sinon | None built in | Yes | Runs in browsers as well as Node | MIT |
Head-to-head comparisons
Vitest vs JestFAQ
Frequently asked questions
What is Vitest?
Vitest is a test framework that runs on Vite’s module pipeline, so TypeScript, JSX and ES modules work through the same configuration and plugins as the application. It offers a largely Jest-compatible API with built-in mocking, snapshots and coverage, plus a Browser Mode that runs tests in real browsers. Vitest 5 was released on 3 September 2026 and is free under the MIT licence.
Is Vitest free?
Yes. Vitest is open source under the MIT licence, with no paid edition or feature tiers; Browser Mode, coverage, sharding and the UI are all free. It is developed by VoidZero and community contributors. VoidZero joined Cloudflare in June 2026 and stated that Vitest will remain open source and MIT-licensed.
Vitest vs Jest: which should I choose?
Choose Vitest for projects built with Vite and for TypeScript or ESM-first codebases, since it shares the application’s configuration and needs no separate transform. Choose Jest for CommonJS or Babel-based projects, React Native, or a large existing suite where migration would cost more than it saves. The APIs are similar enough that the build tooling, not the test syntax, usually decides.
Do I need a Vite app to use Vitest?
No. Vitest works in any Node.js project, including back-end services and libraries, and simply uses Vite internally to transform files. Since version 5, vite is a required peer dependency, which npm and pnpm install automatically; Yarn users add it explicitly. A vitest.config.ts file is enough configuration for a project that has no vite.config.
What changed in Vitest 5?
Vitest 5, released on 3 September 2026, focuses on performance and stricter defaults. It requires Node 22.12 and Vite 6.4 or later, turns clearMocks on by default, requires vi.mock at the top level, fails unawaited async assertions, rewrites the benchmarking API and writes reports under .vitest. New features include a Browser Mode trace view, nested projects that inherit the root config, vi.when and mocking of Temporal in fake timers.
Can Vitest run tests in a real browser?
Yes. Browser Mode, stable since version 4, runs tests inside real browsers through a provider: @vitest/browser-playwright for Chromium, Firefox and WebKit, or the community-maintained WebdriverIO provider. A preview provider exists for local viewing only. Browser tests get real layout and events, locators and visual comparisons with toMatchScreenshot, which jsdom-based tests cannot offer.
How hard is it to migrate from Jest to Vitest?
Usually moderate. Most matchers and the describe and test structure carry over, and replacing jest with vi handles much of the code. The work is in the differences: globals, mockReset behaviour, module mock factories, asynchronous vi.importActual, unsupported done callbacks and snapshot headers. Migrate one folder at a time with both runners installed, and follow the official Migrating from Jest guide.
Alternatives
Vitest alternatives worth weighing
Jest
JavaScript testing framework with bundled mocking, coverage and snapshot testing, zero configuration to start.
Mocha
Flexible JavaScript test framework that supplies structure and lets you choose assertions and mocking.
Resources
Where to go next
Picking the wrong tool costs more than picking late.
QAble advises on toolchain decisions and then implements them, with engineers who have run these tools on real delivery. Start with a free QA audit.