Skip to content

What Actually Changes When You Upgrade to Vitest 5

Vitest 5 clears mocks by default, throws on hoisted calls and drops test.sequential. Tested on 4.1.11 vs 5.0.3, with the exact fix for each break.

· · 15 min read

Updated: October 8, 2026

Source code on a dark screen with syntax highlighting

Quick Take

Vitest 5 clears mocks by default, throws on hoisted calls that used to only warn, drops test.sequential, and moves every artifact into .vitest/. Run side by side on 4.1.11 and 5.0.3, the same tests also exposed four quieter changes, and the suite finished 2 to 15 percent faster.

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. Before writing this I went looking for the four breakages below in the repo I work in. It sits on vitest@^5.0.0, and across its 367 *.test.ts files a grep finds no .sequential, no indented vi.mock, and every resolves and rejects chain awaited. Clean on paper, though. The mock-count change is the one a grep can't see.

Where the side-by-side numbers come from. A script written for the October 8 update runs ten small test files and a generated 240-test suite under vitest@4.1.11 and vitest@5.0.3, both on Vite 8.3.4 and Node 22.23.3. Its companion directory in the Coding Dunia code-examples repo has not been published yet, so the method is described inline rather than linked.

Which Versions Does Vitest 5 Need?

Start here, because nothing else matters if the install fails.

RequirementVitest 4Vitest 5
Node.js20.19+22.12+, 24, or 26+
Vite5.0+6.4, 7, or 8
@types/nodeany^22 or >=24
@vitest/coverage-v8same version as vitestsame version as vitest

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.

A desktop computer on a wooden desk in a home office
Photo by Ryland Dean on Unsplash

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.

Laptop, notebook and mug on a desk cleared for a migration morning
Photo by Daniil Komov on Unsplash

Why Do 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.

A test that counts mock calls made by a different test isn't testing anything, it's just a coincidence with a green checkmark, and Vitest 5 clearing mocks by default finally makes that failure visible instead of leaving it to rot in a passing suite.

Share this Post on X Bluesky

Why Must Hoisted Calls 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. In this repo that grep returns nothing: all 148 hoisted calls, vi.mock and vi.hoisted together, sit at the top level.

What Replaces sequential Now That It 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.

Why Do Unawaited Assertions Fail?

An assertion chain through resolves, rejects or toMatchFileSnapshot returns a promise. Forget the await and Vitest 5 fails the test, whatever the assertion would have said.

I originally wrote that Vitest 4 "let the test pass regardless of the outcome". That was too broad, and running both versions side by side on October 8 showed it. Vitest 4.1.11 already failed an unawaited resolves whose assertion was false, after printing a warning that this would become a failure in the next major. What it let through was the unawaited assertion that was true, plus every unawaited toMatchFileSnapshot. Those are the ones that flip:

// Vitest 4.1.11: passes. Vitest 5.0.3: fails with "was not awaited".
expect(Promise.resolve(1)).resolves.toBe(1)

// Fails on both versions, because the assertion itself is wrong
expect(Promise.resolve(1)).resolves.toBe(2)

// Correct everywhere
await expect(loadUser(1)).resolves.toEqual({ id: 1 })

So the green-for-no-reason tests you're hunting for are the missing awaits on assertions that happen to be right today. They're the dangerous kind, because nothing is checking the day the code changes.

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? In this repo's test files every resolves and rejects chain (52 of them) is awaited, so this change finds nothing to fail here. Yours may not be so tidy.

Where Did the Output Move To?

Every generated artifact now lives under one directory:

ArtifactVitest 4Vitest 5
Attachments.vitest-attachements/.vitest/attachments/
Blob reports.vitest-reports/.vitest/blob/
HTML reporthtml/.vitest/
JSON reporterstdout.vitest/json/output.json
JUnit reporterstdout.vitest/junit/output.xml

The reporter rows are worth testing rather than trusting. Run with --reporter=json and no --outputFile: Vitest 4.1.11 prints the whole report to stdout, while Vitest 5.0.3 prints a single line, JSON report written to .../.vitest/json/output.json, and puts nothing parseable there. A pipeline like vitest run --reporter=json | jq now receives a sentence instead of JSON. The docs mention a stdout option that restores the old behavior, which I did not test.

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.

Which Smaller Removals Are Worth Grepping For?

  • Deprecated entry points are gone: vitest/coverage and vitest/reporters moved to vitest/node, vitest/environments and vitest/snapshot to vitest/runtime, and vitest/mocker was removed outright.
  • @vitest/runner was inlined and is no longer published separately.
  • toHaveTextContent is now a strict equality check; use the new toMatchTextContent for partial or regex matches.
  • The WebdriverIO browser provider moved out to the vitest-community organization.
  • VITEST_POOL_ID and VITEST_WORKER_ID are 1-based instead of 0-based, which matters if you shard databases by worker index.
  • vitest --ui requires the token printed in the terminal.

What Do 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.

What Changed Between the Last RC and 5.0.0?

A checklist is only as good as the RC it was written against, so on September 26 I diffed the tags. v5.0.0-rc.4 to v5.0.0 is 10 commits across 52 files. Subtract a CI tweak, a Babel bump, a docs split and the release commit, and six changes reach your suite. None touches the four breakages above.

One deserves a footnote here, because nothing errors. Inline projects inherit the root config by default now, so a plugins array copied from the root, as everyone did under Vitest 4, loads twice. The reported symptom was Vue files compiling to nothing. Stable warns on exactly that, with three ways out: delete the duplicate, set extends: false, or set extends explicitly to say you meant it.

The rest is repair. doMock() mocks apply in queue order. test.describe counts as a suite during static collection, so --project stops skipping those blocks. Coverage survives virtual files prefixed /@fs/. And spies skip already-cleared mocks, which follows from clearMocks flipping to true: the new default made that scan hot enough to delete.

So the September 4 checklist holds unchanged. Migrating against rc.4 was migrating against the release, which is what an RC series is for.

Which Four Changes Did This Guide Miss at First?

Everything above came from reading release notes and grepping one repo. On October 8 I did the part that was missing: the same test files, run under vitest@4.1.11 and vitest@5.0.3, both on Vite 8.3.4 so the only variable is Vitest. Ten small files, one assertion each, default config. Nine of the ten flipped between versions, and the tenth is the unawaited-and-false case that fails on both. Five of the nine were already covered here. The other four rows come from three behaviors this guide never mentioned, and a fourth change, the coverage package, needed a separate check. None of them shows up as a grep hit in your test files.

Parent-Directory Config Lookup Is Gone

Vitest 4 walked up the directory tree until it found a config. Vitest 5 doesn't. I put a vitest.config.mjs with globals: true one folder above a test file that uses test and expect without importing them, then ran each version from the inner folder. Vitest 4 passed. Vitest 5 died with ReferenceError: test is not defined.

The upstream migration page says to pass the config explicitly, so --config ../vitest.config.ts is the repair. Who gets hit? Anyone who runs vitest from inside a package of a monorepo and relied on the root config being found. It fails loudly here, which is the good case. I'd expect a quieter version for a package that doesn't lean on globals, where the root config's include patterns, aliases or setup files simply stop applying. I didn't test that variant.

Name Filters Join With a Greater-Than Sign

-t and --testNamePattern now match against the suite chain and test name joined by > instead of a space. With a describe('cart') holding test('adds an item'):

CommandVitest 4.1.11Vitest 5.0.3
-t 'cart adds an item'runs 1 testruns 0 tests
-t 'cart > adds an item'runs 0 testsruns 1 test

Both runs exit 0. That's the part I'd worry about: a CI script or an npm script that filters by a copied test name goes green on Vitest 5 having executed nothing. If a pipeline depends on a filter, assert on the number of tests that ran, not on the exit code.

toThrow With an Empty String Got Weaker

A string argument to toThrow is a substring check in Vitest 5, and every message contains the empty string. So expect(() => { throw new Error('boom') }).toThrow('') failed on 4.1.11 (expected [Function] to throw error matching /^$/ but got 'boom') and passes on 5.0.3. Nobody writes this on purpose. It appears when the expected message comes from a variable that turned out to be empty, and the assertion silently stops asserting.

The Coverage Package Has to Match Exactly

This one is the least visible, because package.json accepts it. I forced vitest@5.0.3 next to @vitest/coverage-v8@4.1.11 with --legacy-peer-deps and ran vitest run --coverage on a single passing test. The output opens with Loaded vitest@5.0.3 and @vitest/coverage-v8@4.1.11. Running mixed versions is not supported and may lead into bugs. Then the test passes, and the run still exits 1 with an unhandled TypeError: Expected string coverage payload, received object. With both at 5.0.3 the same run is clean.

I went back to the repo I work in after that. Its root manifest asks for vitest@^5.0.0 and @vitest/coverage-v8@^4.1.0, and the pnpm lockfile resolves the pair to 5.0.0 and 4.1.0. That's the combination that broke in the experiment, with a different patch version. I haven't run that repo's coverage script since, so I can't say it fails there, only that pnpm resolved the pair. Check yours: @vitest/coverage-v8 and @vitest/coverage-istanbul declare an exact vitest version as a peer, and a caret range on the old major will not follow you across the upgrade.

Did the Upgrade Make the Suite Faster?

The vendor numbers I quoted in the new features article are the team's own. These are mine, on a generated 240-test suite that uses vi.fn, vi.mock with a factory and fake timers, laid out as 60 files of 4 tests and as 10 files of 24. Medians of seven rounds with the two versions interleaved inside each round, wall-clock seconds for the whole process, on an Apple M1 with 8 cores, 16 GB and Node 22.23.3. Cold means node_modules/.vite and .vitest were deleted first.

LayoutStartVitest 4.1.11Vitest 5.0.3Change
60 files, default poolcold2.091.87-10%
60 files, default poolwarm1.951.82-7%
10 files, default poolcold0.710.69-2%
10 files, default poolwarm0.770.68-12%
60 files, --pool=vmThreadscold1.110.95-15%
60 files, --pool=vmThreadswarm1.101.02-7%

Vitest 5 was faster in all six cells, by 2 to 15 percent. That's a smaller range than the 8 to 25 percent in the team's announcement, and I'd trust the direction more than any single number. The Mac was busy with other jobs during this run (one-minute load average 7.3 rising to 9.5 on eight cores), which is why a 10-file cold start reads -2% and its warm twin -12%: that gap is noise, not a finding. An earlier run of the same script at a load of 4 to 6 gave the same direction with every cell between -4% and -10%.

What the table doesn't say matters as much. All 240 tests passed on both versions, because I wrote them to avoid the behaviors that flip, so this measures the runner and not your migration. Tests that do almost nothing also aren't a component suite with jsdom and real imports. Don't promise your team 10 percent. Promise them it didn't get slower.

What Did the Patch Releases Add?

Vitest 5.0.1 (September 15), 5.0.2 (September 25) and 5.0.3 (September 30) are bug-fix releases, with two Trace View additions in 5.0.1, and I found no new breaking change in their release notes. Two fixes sit close to things this guide already warned about. 5.0.1 made a self-referencing extends share the server instead of booting a second one, which belongs with the project-inheritance footnote in the release-candidate section. And 5.0.3 lists a cache fix titled "Revalidate imports of cached modules", worth having if you turned on fsModuleCache for the speedup. If you pinned 5.0.0 after the upgrade, move to 5.0.3. Beyond 5.0.5 there is a 5.1.0-beta.1 with --last-failed and --since-last-run; I tried both in Vitest 5.1 Beta Tested.

In What Order Should You Migrate?

Bump Node first, then Vite, then Vitest, running the suite between each step so failures have one obvious cause. Upgrade @vitest/coverage-v8 or @vitest/coverage-istanbul in the same commit as vitest and pin them to the same version. 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. Read your npm scripts and CI files for three things a grep of the test files can't see: a -t filter, a --reporter=json pipe, and a vitest call made from a subfolder. 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.

Quick upgrade or slow one? That depends entirely on how honest your mocks were before.

Frequently Asked Questions

Does Vitest 5 require a new Node.js version?
Yes. The published package declares engines of ^22.12.0 || ^24.0.0 || >=26.0.0, so Node 20 is out and even Node 22.11 is too old. It also needs Vite 6.4 or newer, with Vite 7 and 8 accepted as peers. Check both before you touch a single test file, because a version mismatch produces install errors rather than useful test failures.
Why do my tests suddenly see fewer mock calls?
Because clearMocks now defaults to true, so Vitest runs vi.clearAllMocks() before each test. Call history no longer leaks between tests. If a test asserted toHaveBeenCalledTimes(2) while counting a call made in an earlier test, it now sees 1. Set clearMocks: false in your config to restore the old behavior, though the leaking counts were usually hiding a bug.
Where did my coverage and JUnit output go?
Into the .vitest directory. Attachments moved from .vitest-attachements/ to .vitest/attachments/, blob reports to .vitest/blob/, the HTML report to .vitest/, JSON reporter output to .vitest/json/output.json, and JUnit XML to .vitest/junit/output.xml. CI jobs that upload a hardcoded path will silently upload nothing, so update those globs during the upgrade rather than after.
Is Vitest 5 faster than Vitest 4?
In a side-by-side run of the same generated 240-test suite, yes, by 2 to 15 percent across six configurations: 1.87 against 2.09 seconds cold with 60 files on the default pool, and 0.95 against 1.11 seconds with the vmThreads pool. The machine was busy, so trust the direction more than any single cell. The Vitest team's own published range is 8 to 25 percent.
Why does coverage fail after I upgrade to Vitest 5?
Most likely @vitest/coverage-v8 is still on the 4.x line. The coverage packages declare an exact vitest version as a peer, so vitest 5.0.3 next to coverage-v8 4.1.11 prints a mixed-versions warning and exits 1 with 'Expected string coverage payload, received object', even when every test passes. Upgrade both in the same commit and pin them to the same version.
Why does my -t filter run no tests after upgrading?
Vitest 5 joins the suite chain and the test name with ' > ' instead of a space when matching a name pattern. A filter such as 'cart adds an item' matched in 4.1.11 and runs zero tests in 5.0.3, yet the process still exits 0. Rewrite the pattern as 'cart > adds an item', and check the number of tests that ran instead of the exit code.