Skip to content
Release notesTesting

Vitest 5 release candidate: what's coming for Vue tests

Vitest 5.0.0-rc.1 was published on 11 August, after four months of betas. Vitest 4.1 is still the stable release. Here is what the release candidate changes in a Vue project and what to fix before you switch.

vuejs.school team13 Aug 20266 min readVitest 4.1 stable · Vitest 5.0.0-rc.1

Vitest 5.0.0-rc.1, the first release candidate of the next major version of Vue's usual test runner, was published to npm on 11 August. It follows seven betas, the first on 23 April. A release candidate is a build for testing before the stable release, not the stable release itself: Vitest 4.1 still is, and that is what you should run in CI today.

If your Vue project tests with Vitest, two changes will touch the most test files: mocks are cleared before every test, and an async assertion you forget to await now fails the test. You can fix both on 4.1 before you switch. The facts here come from the rc.1 release notes, the beta release notes before it, and the migration guide as it stands at the rc.1 tag.

Check Node 22.12 and Vite 6.4 first

The migration guide opens with prerequisites: Vitest 5 needs Node.js 22.12 or newer and Vite 6.4 or newer. The package's engines field reads ^22.12.0 || ^24.0.0 || >=26.0.0, and its Vite peer range is ^6.4.0 || ^7.0.0 || ^8.0.0. Node 20 is out. A create-vue project on Vite 8 meets the Vite requirement already; check node --version locally and in CI before anything else.

Mocks are cleared before every test

clearMocks is now on by default. Before every test, Vitest calls vi.clearAllMocks(), which empties each mock's recorded calls, instances and results. The guide is explicit that implementations stay intact, so a mockResolvedValue you set up survives. Only the history goes. A test that counted calls made in an earlier test now sees a different number. In this file, UserProfile calls fetchUser once when it mounts and once more when Refresh is clicked:

src/components/__tests__/UserProfile.spec.tsBreaks
import { flushPromises, mount } from '@vue/test-utils'
import { expect, it, vi } from 'vitest'
import { fetchUser } from '../../api'
import UserProfile from '../UserProfile.vue'

vi.mock('../../api', () => ({ fetchUser: vi.fn().mockResolvedValue({ name: 'Ada' }) }))

it('loads the user on mount', async () => {
  mount(UserProfile)
  await flushPromises()
  expect(fetchUser).toHaveBeenCalledTimes(1)
})

it('loads again when Refresh is clicked', async () => {
  const wrapper = mount(UserProfile)
  await flushPromises()
  await wrapper.find('button').trigger('click')
  expect(fetchUser).toHaveBeenCalledTimes(3)
})

On Vitest 4 the second test passes, because the call from the first test is still on the mock: one from before, plus two of its own. On Vitest 5 the history is cleared before the second test starts, the mock reports two calls, and the assertion fails. The fix is to count only what this test did:

src/components/__tests__/UserProfile.spec.tsWorks
it('loads again when Refresh is clicked', async () => {
  const wrapper = mount(UserProfile)
  await flushPromises()
  await wrapper.find('button').trigger('click')
  expect(fetchUser).toHaveBeenCalledTimes(2)
})

This version relies on cleared mocks, so on Vitest 4.1, which keeps the history by default, it sees three calls and fails. To make it pass on both majors, turn the new default on early: set clearMocks: true in the test options of your 4.1 vitest.config.ts, or clear in an afterEach(() => vi.clearAllMocks()) hook. On Vitest 5 that hook becomes redundant and can go.

The guide warns that calls recorded outside the test body are hit hardest: in a setup file, at the top of a module, or in a beforeAll hook. That history is cleared before the test that asserts on it runs. If your suite mounts once in beforeAll and asserts on mocks in several tests, move the mount into each test or into beforeEach.

To keep the old behaviour while you migrate, set clearMocks: false in vitest.config.ts.

Unawaited async assertions fail the test

Assertions that return a promise, such as .resolves, .rejects and toMatchFileSnapshot, used to be awaited for you at the end of the test, with a warning. In rc.1 they fail, and the error points at the assertion you forgot to await. Store actions and composables that return promises are where this shows up in a Vue project:

src/stores/__tests__/todos.spec.tsBreaks
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, expect, it } from 'vitest'
import { useTodoStore } from '../todos'

beforeEach(() => {
  setActivePinia(createPinia())
})

it('rejects an empty title', async () => {
  const todos = useTodoStore()
  expect(todos.add('')).rejects.toThrow('Title is required')
})
src/stores/__tests__/todos.spec.tsWorks
it('rejects an empty title', async () => {
  const todos = useTodoStore()
  await expect(todos.add('')).rejects.toThrow('Title is required')
})

You can fix every one of these on Vitest 4.1 today. Run your suite and search the output for "was not awaited": 4.1 prints that warning, with a stack trace, for each assertion it had to await for you. Each one will fail on 5, so add the await.

Other breaking changes to search for

The guide lists many more. These are the ones most likely to appear in a Vue project:

  • vi.mock must be at the top level. A vi.mock, vi.unmock or vi.hoisted call inside a describe or a function used to log a warning; now it throws, listing each offending call and its line. vi.doMock is not hoisted and may still go anywhere.
  • -t matches the full name joined with >. vitest -t 'TodoList adds' becomes vitest -t 'TodoList > adds', the same string the reporter prints.
  • test.sequential and describe.sequential are removed. Use { concurrent: false } in the options instead.
  • expect.poll fails when it times out, even if a late attempt would have passed.
  • toThrow('') matches any message. Use toThrow(/^$/) to assert an empty one.
  • Reports and artifacts move into one .vitest folder, and the json and junit reporters write files there by default instead of printing to stdout. Scripts that pipe --reporter=json into another tool need the stdout option.
  • Config files are no longer looked up in parent directories. Running vitest from a subfolder needs --config.
  • Inline projects inherit the root config by default, Vite plugins included, and share its Vite server unless they change Vite options. A @vitejs/plugin-vue in the root config now reaches every inline project, which is usually what you wanted all along.

Browser Mode changes

If your component tests run in a real browser, three changes in the guide affect them:

  • Locators now match text exactly by default: the full string, case-sensitive. browser.locators.exact: false restores the old matching.
  • The toHaveTextContent assertion now means strict equality. The old partial and regular-expression matching moved to a new toMatchTextContent.
  • The WebdriverIO provider has moved to a community-maintained package.

The guide also says that render from vitest-browser-vue will return a promise that you must await. The current vitest-browser-vue release, 2.1, still declares Vitest 4 as its peer, so that change arrives with a new major of the companion package. Browser Mode suites built on it should wait for that release before trying the RC.

Browser Mode gains an experimental Trace View as well. Set browser.traceView: true and Vitest records every action and assertion as a DOM snapshot that you can replay step by step in the browser UI, Vitest UI or the HTML report. With the HTML reporter in CI, a failure then comes with the page as it looked at each step.

New helpers for mocks, time and the CLI

Three smaller additions are useful in Vue projects:

  • vi.when(spy).calledWith(...).thenResolve(...) gives a mock different behaviour per argument. It replaces a hand-written mockImplementation with a switch inside.
  • Fake timers now also fake Temporal when it is available (natively from Node 26, or through a global polyfill), so code that reads the time through Temporal.Now follows vi.setSystemTime.
  • The CLI gains -p as a shorthand for --project, and a --repeats option for flushing out flaky tests.

What stays the same

Vue Test Utils 2.4 does not depend on the runner, so mount, trigger, emitted and flushPromises behave as before. The happy-dom and jsdom environments stay, with one refinement from the guide: assigning to a global such as window.innerWidth now updates the underlying DOM implementation, so matchMedia in happy-dom sees the new width.

How to try it

Do it on a branch, after your suite is green on 4.1. Install exact versions: rc.1 declares its companion packages, such as @vitest/coverage-v8, as peers at exactly 5.0.0-rc.1.

git switch -c try-vitest-5
npm install -D vitest@5.0.0-rc.1 @vitest/coverage-v8@5.0.0-rc.1
npx vitest run

Add @vitest/ui@5.0.0-rc.1 if you use the UI. Expect the first run to fail in two groups: call counts (clearMocks) and unawaited assertions. Fix both kinds on the branch, then bring the fixes back to your main branch with clearMocks: true in its config, so the new counts pass on 4.1 too. When 5.0 is stable, those two groups of failures are already fixed. If you find a bug, report it now: that is what a release candidate is for.

Check it stuck3 questions from the Vitest and Vue Test Utils quiz.
Practice now
Was this helpful?

Keep reading