Vitest, the test runner create-vue offers for unit tests, published version 5.0.0 on 3 September, after a release candidate phase that started on 11 August. The announcement names performance as the main focus, and the migration guide lists the breaking changes.
For a Vue suite, three of them are the most likely to turn passing tests red: mocks lose their call history before every test, unawaited resolves and rejects assertions fail, and Browser Mode tests need an awaited render and exact text.
Faster runs, Trace View and vi.when
The team built a benchmark set of generated apps and compared Vitest 4.1.10 with 5.0. A few of the published results:
- A Vue single-page app with 37 components in Browser Mode on Chromium: 1.94 s down to 1.58 s, 18% faster.
- An enterprise-sized app with 1,280 modules, forks pool, isolated: 7.24 s down to 5.83 s.
- A dependency-heavy app on the
vmThreadspool: 1.59 s down to 0.74 s.
Not every suite gains: runs dominated by environment setup, such as forks with jsdom and isolation, stay within 3% of 4.1.
To see where your own suite spends its time, read the reporter's duration line, which now shows percentages (environment, import, transform, tests). The new vitest doctor command goes further: it reruns your suite with other settings and suggests faster options.
Two additions help when you write and debug tests:
- Trace View for Browser Mode. With
browser.traceView: true, Vitest records every interaction and assertion as a DOM snapshot that you can step through after a run, in the browser UI, Vitest UI or the HTML reporter. vi.whensets return values per argument on a spy:vi.when(fetchUser).calledWith(1).thenResolve({ id: 1, name: 'Ada' }).
Mocks are cleared before every test
clearMocks now defaults to true. Before each test, Vitest calls vi.clearAllMocks(), which empties the call history of every mock but keeps its implementation. Tests can no longer depend on calls made by an earlier test, which removes a common cause of tests that pass or fail depending on their order.
The guide warns which tests are hit: those that record calls outside the test body, in a setup file, at the top of a module or in beforeAll. A Vue suite that mounts once and asserts many times is exactly that. Here SignupForm calls its track prop with 'signup_viewed' when it mounts:
import { mount } from '@vue/test-utils'
import { beforeAll, expect, it, vi } from 'vitest'
import SignupForm from '../SignupForm.vue'
const track = vi.fn()
beforeAll(() => {
mount(SignupForm, { props: { track } })
})
it('reports that the form was shown', () => {
expect(track).toHaveBeenCalledWith('signup_viewed')
})
On Vitest 4 this passes. On Vitest 5 the call made during mount is cleared before the test runs, and the assertion fails with "Number of calls: 0". Mount where you assert:
it('reports that the form was shown', () => {
const track = vi.fn()
mount(SignupForm, { props: { track } })
expect(track).toHaveBeenCalledWith('signup_viewed')
})
The fixed test also passes on Vitest 4, and it reads better: everything it checks is set up in front of you. If you had an afterEach(() => vi.clearAllMocks()), you can delete it.
To get the old behaviour back while you work through a large suite, set clearMocks: false in vitest.config.ts.
Unawaited assertions and nested vi.mock now fail
Two mistakes that Vitest 4 only warned about are now errors.
Unawaited async assertions. resolves, rejects and toMatchFileSnapshot return promises. Vitest 4 awaited a forgotten one at the end of the test and printed a warning, so the assertion ran at a different moment from where you wrote it. Vitest 5 fails the test and points at the line:
import { expect, it } from 'vitest'
import { loadCart } from '../loadCart'
it('loads the saved cart', async () => {
expect(loadCart()).resolves.toEqual(['Milk']) // fails on Vitest 5
await expect(loadCart()).resolves.toEqual(['Milk']) // the fix
})
The first line fails the test on Vitest 5 even though the promise resolves to the right value. Search your suite for .resolves and .rejects without an await in front.
vi.mock inside a function. vi.mock, vi.unmock and vi.hoisted are moved to the top of the file before anything runs, so a call written inside describe never ran where it appeared. Vitest 5 fails the whole file and lists each call with its location. Move it to the top level. vi.doMock, which is not hoisted, can still go anywhere.
Browser Mode: async render and exact locators
Browser Mode runs component tests in a real browser through a provider package. With Vitest 5 you need @vitest/browser-playwright 5, which must match the Vitest version exactly, and vitest-browser-vue 3, published on 4 September. Three changes hit Vue tests:
renderfrom vitest-browser-vue now returns a promise and must be awaited.- Locators match text exactly by default.
getByText('Buy')no longer finds "Buy milk". toHaveTextContentchecks for exact equality and no longer accepts a regular expression. Partial and regex matches moved to the newtoMatchTextContent.
This test for a TodoList component, which renders its items as a list, hits all three on Vitest 5. The second block is the fixed version:
import { expect, test } from 'vitest'
import { render } from 'vitest-browser-vue'
import TodoList from '../TodoList.vue'
test('shows the items', async () => {
const screen = render(TodoList, { props: { items: ['Buy milk', 'Walk the dog'] } })
await expect.element(screen.getByText('Buy')).toBeVisible()
await expect.element(screen.getByRole('list')).toHaveTextContent('milk')
})
import { expect, test } from 'vitest'
import { render } from 'vitest-browser-vue'
import TodoList from '../TodoList.vue'
test('shows the items', async () => {
const screen = await render(TodoList, { props: { items: ['Buy milk', 'Walk the dog'] } })
await expect.element(screen.getByText('Buy milk')).toBeVisible()
await expect.element(screen.getByRole('list')).toMatchTextContent('milk')
})
To ease the move, browser.locators.exact: false restores partial matching. Exact matching is the better default, though: a test that finds "Buy milk" when you asked for "Buy" could just as easily find the wrong row.
When a locator finds nothing, Vitest 5 also prints the accessibility tree of the searched area. It is usually shorter than the HTML and shows the roles and names getByRole matches against.
Other breaking changes to check
-tpatterns match the full name joined with' > ', as the reporter shows it.vitest -t 'cart adds'becomesvitest -t 'cart > adds'. An old pattern that spans adescribeand a test name now matches nothing: its tests are skipped and the run still passes.- Inline projects in
test.projectsinherit the root config, plugins and aliases included, withoutextends: true. That helps when your Vue plugin is declared once at the root. Arrays are merged, so a project'ssetupFilesare added to the root's; setextends: falseon a project to keep the old behaviour. - Reports and artifacts go to one
.vitestfolder: the HTML report, failure screenshots and attachments. Thejsonandjunitreporters now write files there instead of printing to the terminal, so a CI script that piped their output should read the file instead. toThrow('')matches any error message, because an empty string is a substring of every string (Vitest 4 matched only an empty message).expect.pollfails when it times out.- Vitest UI needs the token URL it prints when it starts.
- Config files are no longer looked up in parent folders.
VITEST_POOL_IDandVITEST_WORKER_IDstart at 1, not 0.- Fake timers and
vi.setSystemTime()also fakeTemporalnow. benchis no longer a top-level import: in benchmark files it is a fixture you receive insidetest().
Upgrading a Vue project
Check the two requirements and start from a green suite. Then move every Vitest package together, because Vitest 5 expects its coverage, UI and browser packages at exactly its own version:
node --version
npx vite --version
npx vitest --run
npm install -D vitest@^5.0.0 @vitest/coverage-v8@^5.0.0
If you use Browser Mode, add @vitest/browser-playwright@^5.0.0 and vitest-browser-vue@^3.0.0 to that install line, and add @vitest/ui@^5.0.0 if you use Vitest UI. Vue Test Utils does not depend on the runner, so mount and wrapper code is untouched.
Run the suite and work through the failures in this order: unawaited assertions first (the error names the line), then mock history, then locators. Some changes fail no test, so check these by hand: -t patterns in your package.json scripts, and CI steps that read json or junit output from the terminal.
Upgrade now if your machines and CI already run Node 22.12 or newer: each fix above is a small edit, and most of the benchmark apps ran faster. If CI is still on Node 20, move Node first; Vitest 4.1 keeps working in the meantime.
Upgrading a Nuxt project
Nuxt tests run through @nuxt/test-utils, which declares the Vitest versions it supports. Support arrived in three releases on 7 September: 4.3.0 added Vitest 5 support, 4.3.1 added ^5.0.0 to its peer range, and 4.3.2 fixed the test environment. Upgrade both together:
npm install -D vitest@^5.0.0 @nuxt/test-utils@^4.3.2
With 4.3.0 or older, npm refuses the install because the module's peer range still reads ^4.0.2. Everything in the sections above applies inside a Nuxt test suite as well.