Vitest 5.0.0 was published on September 3, 2026, eleven months after 4.0.0 landed in October 2025. It requires Node.js 22.12 or newer and Vite 6.4 or newer, clears mocks before every test by default, removes test.sequential, turns several old warnings into thrown errors, and moves every generated artifact into a single .vitest/ directory. I upgraded two suites the morning it dropped. One took nine minutes; the other took an hour, entirely because of mock call counts that had been quietly wrong for months.
Quick take: Vitest 5 needs Node 22.12+ and Vite 6.4+. Four changes cause almost all the breakage:
clearMocksdefaults totrue,vi.mock/vi.hoistedthrow when they are not at the top level,test.sequentialanddescribe.sequentialare gone in favor of{ concurrent: false }, and unawaitedresolves/rejectsassertions now fail instead of passing silently. Output paths moved under.vitest/, which breaks CI artifact uploads before it breaks any test.
Which Versions Does Vitest 5 Need?
Start here, because nothing else matters if the install fails.
| Requirement | Vitest 4 | Vitest 5 |
|---|---|---|
| Node.js | 20.19+ | 22.12+, 24, or 26+ |
| Vite | 5.0+ | 6.4, 7, or 8 |
@types/node | any | ^22 or >=24 |
Node 20 hit end of maintenance in April 2026, so this is less aggressive than it looks. Still on v20? That upgrade comes first, and the Node 20 to 24 migration path has its own set of removals. The Vite floor matters for anyone running multi-target builds through the Vite 6 Environment API, since Vitest 5 resolves projects through that machinery.
Half of these version floors trace back to your compiler settings rather than to Vitest. If moduleResolution and lib in your tsconfig.json still describe a Node 18 world, our tsconfig generator produces a baseline that matches the runtime you actually target, which removes a whole category of confusing resolution errors before you start.
Mocks Clear Themselves Now
This is the change that will cost you the most time. clearMocks used to default to false; it now defaults to true, and Vitest calls vi.clearAllMocks() before each test.
// Vitest 4: call history survived across tests
const fn = vi.fn()
test('first', () => {
fn()
})
test('second', () => {
fn()
expect(fn).toHaveBeenCalledTimes(2) // passed, counting both tests
})
// Vitest 5: history is cleared before each test
test('second', () => {
fn()
expect(fn).toHaveBeenCalledTimes(1) // only this test's call
})
Implementations set with mockImplementation survive. Only the recorded calls go.
Restoring the Old Behavior
One config line brings v4 semantics back:
// vitest.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: { clearMocks: false },
})
Should you? I'd say no, and I'll defend it: a test that depends on a call made by a different test is not a test, it's a coincidence with a green checkmark. Use the escape hatch to unblock a release, then fix the assertions properly.
Hoisted Calls Must Be Top Level
vi.mock(), vi.unmock() and vi.hoisted() are hoisted to the top of the file at transform time. Calling them inside a describe block never worked the way people expected. Vitest 4 warned. Vitest 5 throws.
// Throws in Vitest 5
describe('calculator', () => {
vi.mock('./calculator')
})
// Correct
vi.mock('./calculator')
describe('calculator', () => {
// ...
})
Grep for vi.mock with leading whitespace and you'll find every offender in about ten seconds.
sequential Is Gone
test.sequential and describe.sequential were removed. The options object replaces them.
// Removed in Vitest 5
test.sequential('writes the file', async () => {})
describe.sequential('db suite', () => {})
// Replacement
test('writes the file', { concurrent: false }, async () => {})
describe('db suite', { concurrent: false }, () => {})
Same behavior, one spelling instead of two. Hard to argue with that.
Unawaited Assertions Fail
An assertion chain through resolves, rejects or toMatchFileSnapshot returns a promise. Forget the await and Vitest 4 let the test pass regardless of the outcome. Now it fails the test.
// Passes in v4 even when the promise rejects, fails in v5
expect(loadUser(1)).resolves.toEqual({ id: 1 })
// Correct
await expect(loadUser(1)).resolves.toEqual({ id: 1 })
expect.poll changed too: it rejects when the callback doesn't settle in time, and the callback receives an AbortSignal you can hand to fetch.
await expect.poll(async ({ signal }) => {
const res = await fetch('/api/status', { signal })
return res.status
}, { timeout: 1000 }).toBe(200)
How many of your integration tests have been green for reasons nobody verified? Mine had two. Both were real bugs.
Output Moved Into .vitest
Every generated artifact now lives under one directory:
| Artifact | Vitest 4 | Vitest 5 |
|---|---|---|
| Attachments | .vitest-attachements/ | .vitest/attachments/ |
| Blob reports | .vitest-reports/ | .vitest/blob/ |
| HTML report | html/ | .vitest/ |
| JSON reporter | stdout | .vitest/json/output.json |
| JUnit reporter | stdout | .vitest/junit/output.xml |
Note the old attachments path was misspelled. That typo is finally gone. Update .gitignore and any CI upload step that references the old paths, because an upload glob matching nothing does not fail a build, it just quietly produces an empty artifact. If your pipeline compares Vitest against the platform runner, the same caveat applies to the paths described in our Node test runner comparison.
Smaller Removals Worth Grepping For
- Deprecated entry points are gone:
vitest/coverageandvitest/reportersmoved tovitest/node,vitest/environmentsandvitest/snapshottovitest/runtime, andvitest/mockerwas removed outright. @vitest/runnerwas inlined and is no longer published separately.toHaveTextContentis now a strict equality check; use the newtoMatchTextContentfor partial or regex matches.- The WebdriverIO browser provider moved out to the vitest-community organization.
VITEST_POOL_IDandVITEST_WORKER_IDare 1-based instead of 0-based, which matters if you shard databases by worker index.vitest --uirequires the token printed in the terminal.
What You Get Back
The benchmark API was rewritten so bench is a test-context fixture with access to fixtures, hooks and assertions, rather than a top-level import. Fake timers now mock the Temporal API alongside Date, with toNotFake: ['Temporal'] as the opt-out. Class mocks keep prototype methods, so new MockedDog() instanceof Dog is finally true. And vi.when adds per-argument stubbing:
vi.when(findById).calledWith(1).thenResolve({ id: 1, name: 'Ella' })
That one deletes a lot of hand-rolled mockImplementation switch statements.
The Order That Worked for Me
Bump Node first, then Vite, then Vitest, running the suite between each step so failures have one obvious cause. Next, set clearMocks: false temporarily and confirm the suite is green, which separates version problems from mock problems. Then flip it back to true and fix what breaks. Finally, update CI paths and .gitignore for .vitest/. Read the full release notes before starting, especially if you use Browser Mode, where locators became strict and render() turned async.
Nine minutes or an hour. Which one you get depends entirely on how honest your mocks were before.