---
title: "Vitest 3 is out: what changes for your Vue tests · vuejs.school"
description: "Vitest 3.0 brings a rewritten reporter, inline workspaces and line-number filtering. Most Vue tests pass unchanged; a few spy and timer habits do not."
canonical_url: "https://vuejs.school/community/blog/vitest-3-what-changes-for-vue-tests"
meta:
  "og:description": "Vitest 3.0 brings a rewritten reporter, inline workspaces and line-number filtering. Most Vue tests pass unchanged; a few spy and timer habits do not."
  "og:image": "https://vuejs.school/_og/d/a_~Vml0ZXN0IDMgaXMgb3V0OiB3aGF0IGNoYW5nZXMgZm9yIHlvdXIgVnVlIHRlc3RzIMK3IHZ1ZWpzLnNjaG9vbA%2Cc_OgCard.takumi%2Ctitle_~Vml0ZXN0IDMgaXMgb3V0OiB3aGF0IGNoYW5nZXMgZm9yIHlvdXIgVnVlIHRlc3Rz%2Cdescription_~Vml0ZXN0IDMuMCBicmluZ3MgYSByZXdyaXR0ZW4gcmVwb3J0ZXIsIGlubGluZSB3b3Jrc3BhY2VzIGFuZCBsaW5lLW51bWJlciBmaWx0ZXJpbmcuIE1vc3QgVnVlIHRlc3RzIHBhc3MgdW5jaGFuZ2VkOyBhIGZldyBzcHkgYW5kIHRpbWVyIGhhYml0cyBkbyBub3Qu%2Ceyebrow_~UmVsZWFzZSBub3RlcyDCtyA2IG1pbiByZWFk%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy92aXRlc3QtMy13aGF0LWNoYW5nZXMtZm9yLXZ1ZS10ZXN0cyI%2Cs_mYKym78roAHz-wRr.png"
  "og:image:alt": "Vitest 3 is out: what changes for your Vue tests · vuejs.school"
  "og:image:height": "600"
  "og:image:type": image/png
  "og:image:width": "1200"
  "og:title": "Vitest 3 is out: what changes for your Vue tests · vuejs.school"
  "twitter:image": "https://vuejs.school/_og/d/a_~Vml0ZXN0IDMgaXMgb3V0OiB3aGF0IGNoYW5nZXMgZm9yIHlvdXIgVnVlIHRlc3RzIMK3IHZ1ZWpzLnNjaG9vbA%2Cc_OgCard.takumi%2Ctitle_~Vml0ZXN0IDMgaXMgb3V0OiB3aGF0IGNoYW5nZXMgZm9yIHlvdXIgVnVlIHRlc3Rz%2Cdescription_~Vml0ZXN0IDMuMCBicmluZ3MgYSByZXdyaXR0ZW4gcmVwb3J0ZXIsIGlubGluZSB3b3Jrc3BhY2VzIGFuZCBsaW5lLW51bWJlciBmaWx0ZXJpbmcuIE1vc3QgVnVlIHRlc3RzIHBhc3MgdW5jaGFuZ2VkOyBhIGZldyBzcHkgYW5kIHRpbWVyIGhhYml0cyBkbyBub3Qu%2Ceyebrow_~UmVsZWFzZSBub3RlcyDCtyA2IG1pbiByZWFk%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy92aXRlc3QtMy13aGF0LWNoYW5nZXMtZm9yLXZ1ZS10ZXN0cyI%2Cs_mYKym78roAHz-wRr.png"
  "twitter:image:alt": "Vitest 3 is out: what changes for your Vue tests · vuejs.school"
---

> Index of every page on this site: [llms.txt](https://vuejs.school/llms.txt)

## What shipped

The announcement names five headline changes:

- The terminal reporter was rewritten. Output flickers less and stays stable while tests run. The reporter API behind the `reporters` option was redesigned with a simpler lifecycle: the old `onTaskUpdate` hook still works but is deprecated in favour of hooks such as `onTestCaseResult`.
- Workspaces can be declared inline. A monorepo lists its projects straight in the config file with `workspace: ['packages/*']`, with no separate workspace file.
- Browser mode gets `browser.instances`, an array that runs the same tests in several browsers from one config and one Vite server.
- You can run a single test by its line number: `vitest src/Counter.spec.ts:12`.
- The `vitest/node` API was redesigned and documented, and the team plans to drop its experimental label in the next minor.

The [changelog](https://github.com/vitest-dev/vitest/releases/tag/v3.0.0) adds smaller features: the new matchers `toBeOneOf` and `toHaveBeenCalledExactlyOnceWith`, `expect.toSatisfy()` as an asymmetric matcher, a `describe.for` that mirrors `test.for`, and a `summary` option for the default reporter.

## Breaking changes that can fail a component test

The announcement calls the breaking changes small, and for a typical Vue project that is true. The migration guide linked from the announcement lists them in full. These four can reach a component test.

**`mockReset()` now restores the original implementation.** Before, `mockReset()` turned a mock into an empty function that returns `undefined`. Now a mock created with `vi.fn(() => 'hello')` goes back to returning `'hello'` after a reset. If your `afterEach` relied on a reset to silence a mock, the mock now keeps its behaviour.

**`vi.spyOn()` reuses an existing spy.** Spying on a method that is already spied on returns the same spy instead of wrapping it again, so `mockRestore()` restores the real method rather than the previous spy. This fixes a class of leaky tests, but a test that counted on two separate spies will see one.

**Fake timers fake more by default.** `vi.useFakeTimers()` used to replace a fixed list: `setTimeout`, `clearTimeout`, `setInterval`, `clearInterval`, `setImmediate`, `clearImmediate` and `Date`. Now it replaces every timer-related API it finds except `process.nextTick`. That includes `performance.now()` and, in a DOM environment such as happy-dom or jsdom, `requestAnimationFrame`.

A component that measures elapsed time with `performance.now()` will see time stand still until you advance it. To get the old list back for every test, set it in your config:

*vitest.config.ts*

```
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    fakeTimers: {
      toFake: ['setTimeout', 'clearTimeout', 'setInterval', 'clearInterval', 'setImmediate', 'clearImmediate', 'Date'],
    },
  },
})
```

`toFake` names exactly what gets faked. To change one test only, pass the same option to that call instead: `vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] })`.

**Error equality is stricter.** `toEqual` and `toThrowError` now compare an error's `name`, `message` and `cause`, and check the prototype. `expect(fn).toThrowError(new Error('Bad input'))` fails when the thrown error is a `TypeError`, where before it passed. Match the class you actually throw.

Two smaller changes do not fail a test:

- Passing options as the third argument, `it('saves', async () => {}, { timeout: 10_000 })`, is deprecated. Put the object second: `it('saves', { timeout: 10_000 }, async () => {})`. A plain number as the third argument still works. The next major version will turn the warning into an error, so move the options now.
- Test files are always excluded from the coverage report, even if you overrode `coverage.exclude`. If your coverage number moves a little, that is why.

## Fix a test that relied on mockReset

`ShoppingList` takes a `load` function as a prop, calls it once, and lists what it returns. When `load` returns nothing, it shows an empty state.

*src/components/ShoppingList.vue*

```
<script setup lang="ts">
const props = defineProps<{
  load: () => string[] | undefined
}>()

const items = props.load() ?? []
</script>

<template>
  <ul v-if="items.length">
    <li v-for="item in items" :key="item">{{ item }}</li>
  </ul>
  <p v-else>Nothing to buy</p>
</template>
```

Its test file resets the mock after each test. It passed on Vitest 2 and fails on Vitest 3:

*src/components/\_\_tests\_\_/ShoppingList.spec.ts**Breaks***

```
import { mount } from '@vue/test-utils'
import { afterEach, expect, it, vi } from 'vitest'
import ShoppingList from '../ShoppingList.vue'

const load = vi.fn(() => ['Milk', 'Eggs'])

afterEach(() => {
  load.mockReset()
})

it('renders the items', () => {
  const wrapper = mount(ShoppingList, { props: { load } })
  expect(wrapper.text()).toContain('Milk')
})

it('shows the empty state', () => {
  const wrapper = mount(ShoppingList, { props: { load } })
  expect(wrapper.text()).toContain('Nothing to buy')
})
```

The second test never said what `load` should return. It leaned on `mockReset()` having turned `load` into a function that returns `undefined`, which the component turns into an empty list. On Vitest 3 the reset restores the `['Milk', 'Eggs']` implementation, the list renders, and the assertion fails with `expected 'MilkEggs' to contain 'Nothing to buy'`.

The fix is to say what you mean. Replace the second test with one that sets its own return value:

*src/components/\_\_tests\_\_/ShoppingList.spec.ts**Works***

```
it('shows the empty state', () => {
  load.mockReturnValue([])
  const wrapper = mount(ShoppingList, { props: { load } })
  expect(wrapper.text()).toContain('Nothing to buy')
})
```

That version passes on both majors, and it reads better: the test states its own setup instead of inheriting it from a hook. Search your suites for `mockReset` and for `vi.fn(` with an implementation, and check each pair for the same hidden dependency.

## What stays the same

Most of what a Vue component test touches is unchanged:

- Vue Test Utils. Version 2.4 is the current release, and `mount`, `shallowMount`, `wrapper.find`, `trigger` and `emitted` behave as before. The library does not depend on the test runner at all.
- The `environment: 'happy-dom'` or `'jsdom'` setting.
- The `vitest.config.ts` that create-vue generates, with `mergeConfig(viteConfig, ...)`. It needs no edit.
- Snapshots. `toMatchSnapshot` works exactly as it did. The change is in the API of the `@vitest/snapshot` package, which now supports several snapshot states in one run and matters only to code that calls it directly.

## How to upgrade

First, the one check: confirm your Vite version with `npx vite --version`. If it prints 5.x or 6.x you are ready. If it prints 4.x, upgrade Vite before touching Vitest, because Vitest 3 depends on Vite 5 or 6.

Then make sure your suite is green on Vitest 2, so every failure after the upgrade is the upgrade's doing:

```
npx vitest --run
```

Now install Vitest and its companion packages in one command. The coverage, UI and browser packages each expect the exact same version as `vitest`, so upgrade them together:

```
npm install -D vitest@3 @vitest/coverage-v8@3
```

Add `@vitest/ui@3` or `@vitest/browser@3` to that line if you use them. Run the suite again, and read the warnings before the failures: the third-argument deprecation prints one warning for each test or `describe` that still passes its options last.

## Upgrade now or wait

Upgrade now if your project is on Vite 5 or 6 and your suite is green. The breaking changes are findable by search, the new reporter is easier to read, and running one test by line number is a small thing you will use often. An app already on Vite 6 gains the most, because Vitest 3 can use the app's own Vite instead of a separate Vite 5.

Wait if you are on Vite 4, and upgrade Vite first. Fake timers are not a reason to wait. If you are unsure what your timer tests depend on, add the `toFake` list above when you upgrade, get the suite green, and remove the list later, one timer test at a time.

**Check it stuck**3 questions from the Vitest and Vue Test Utils quiz.

[**Practice now**](https://vuejs.school/learn/testing/vitest-and-vue-test-utils?tab=quiz)

**Was this helpful?**

## **Keep reading**

[Release notes · 6 min read**Vitest 5: what changed for Vue tests**](https://vuejs.school/community/blog/vitest-5-what-changed-for-vue-tests) [Release notes · 6 min read**Vitest 5 release candidate: what's coming for Vue tests**](https://vuejs.school/community/blog/vitest-5-rc-whats-coming)

```json
{"@context":"https://schema.org","@graph":[{"@id":"https://vuejs.school/#website","@type":"WebSite","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school/","publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/vitest-3-what-changes-for-vue-tests#webpage","@type":"WebPage","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-01-21","description":"Vitest 3.0 brings a rewritten reporter, inline workspaces and line-number filtering. Most Vue tests pass unchanged; a few spy and timer habits do not.","name":"Vitest 3 is out: what changes for your Vue tests","url":"https://vuejs.school/community/blog/vitest-3-what-changes-for-vue-tests","breadcrumb":{"@id":"https://vuejs.school/community/blog/vitest-3-what-changes-for-vue-tests#breadcrumb"},"isPartOf":{"@id":"https://vuejs.school/#website"},"potentialAction":[{"@type":"ReadAction","target":["https://vuejs.school/community/blog/vitest-3-what-changes-for-vue-tests"]}],"primaryImageOfPage":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/#identity","@type":"Organization","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school","logo":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/community/blog/vitest-3-what-changes-for-vue-tests#article","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-01-21","description":"Vitest 3.0 brings a rewritten reporter, inline workspaces and line-number filtering. Most Vue tests pass unchanged; a few spy and timer habits do not.","headline":"Vitest 3 is out: what changes for your Vue tests","inLanguage":"en","isAccessibleForFree":true,"thumbnailUrl":"https://vuejs.school/_og/d/a_~Vml0ZXN0IDMgaXMgb3V0OiB3aGF0IGNoYW5nZXMgZm9yIHlvdXIgVnVlIHRlc3RzIMK3IHZ1ZWpzLnNjaG9vbA%2Cc_OgCard.takumi%2Ctitle_~Vml0ZXN0IDMgaXMgb3V0OiB3aGF0IGNoYW5nZXMgZm9yIHlvdXIgVnVlIHRlc3Rz%2Cdescription_~Vml0ZXN0IDMuMCBicmluZ3MgYSByZXdyaXR0ZW4gcmVwb3J0ZXIsIGlubGluZSB3b3Jrc3BhY2VzIGFuZCBsaW5lLW51bWJlciBmaWx0ZXJpbmcuIE1vc3QgVnVlIHRlc3RzIHBhc3MgdW5jaGFuZ2VkOyBhIGZldyBzcHkgYW5kIHRpbWVyIGhhYml0cyBkbyBub3Qu%2Ceyebrow_~UmVsZWFzZSBub3RlcyDCtyA2IG1pbiByZWFk%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy92aXRlc3QtMy13aGF0LWNoYW5nZXMtZm9yLXZ1ZS10ZXN0cyI%2Cs_mYKym78roAHz-wRr.png","@type":["Article","BlogPosting"],"articleSection":["Release notes"],"author":{"@id":"https://vuejs.school/#identity"},"image":{"@id":"https://vuejs.school/#/schema/image/1"},"isPartOf":{"@id":"https://vuejs.school/community/blog/vitest-3-what-changes-for-vue-tests#webpage"},"mainEntityOfPage":{"@id":"https://vuejs.school/community/blog/vitest-3-what-changes-for-vue-tests#webpage"},"publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/vitest-3-what-changes-for-vue-tests#breadcrumb","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","name":"Blog","item":"https://vuejs.school/community/blog","position":1},{"@type":"ListItem","name":"Vitest 3 is out: what changes for your Vue tests","position":2}]},{"@id":"https://vuejs.school/#logo","@type":"ImageObject","caption":"vuejs.school","contentUrl":"https://vuejs.school/apple-touch-icon.png","inLanguage":"en","url":"https://vuejs.school/apple-touch-icon.png"},{"@id":"https://vuejs.school/#/schema/image/1","@type":"ImageObject","contentUrl":"https://vuejs.school/_og/d/a_~Vml0ZXN0IDMgaXMgb3V0OiB3aGF0IGNoYW5nZXMgZm9yIHlvdXIgVnVlIHRlc3RzIMK3IHZ1ZWpzLnNjaG9vbA%2Cc_OgCard.takumi%2Ctitle_~Vml0ZXN0IDMgaXMgb3V0OiB3aGF0IGNoYW5nZXMgZm9yIHlvdXIgVnVlIHRlc3Rz%2Cdescription_~Vml0ZXN0IDMuMCBicmluZ3MgYSByZXdyaXR0ZW4gcmVwb3J0ZXIsIGlubGluZSB3b3Jrc3BhY2VzIGFuZCBsaW5lLW51bWJlciBmaWx0ZXJpbmcuIE1vc3QgVnVlIHRlc3RzIHBhc3MgdW5jaGFuZ2VkOyBhIGZldyBzcHkgYW5kIHRpbWVyIGhhYml0cyBkbyBub3Qu%2Ceyebrow_~UmVsZWFzZSBub3RlcyDCtyA2IG1pbiByZWFk%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy92aXRlc3QtMy13aGF0LWNoYW5nZXMtZm9yLXZ1ZS10ZXN0cyI%2Cs_mYKym78roAHz-wRr.png","inLanguage":"en","url":"https://vuejs.school/_og/d/a_~Vml0ZXN0IDMgaXMgb3V0OiB3aGF0IGNoYW5nZXMgZm9yIHlvdXIgVnVlIHRlc3RzIMK3IHZ1ZWpzLnNjaG9vbA%2Cc_OgCard.takumi%2Ctitle_~Vml0ZXN0IDMgaXMgb3V0OiB3aGF0IGNoYW5nZXMgZm9yIHlvdXIgVnVlIHRlc3Rz%2Cdescription_~Vml0ZXN0IDMuMCBicmluZ3MgYSByZXdyaXR0ZW4gcmVwb3J0ZXIsIGlubGluZSB3b3Jrc3BhY2VzIGFuZCBsaW5lLW51bWJlciBmaWx0ZXJpbmcuIE1vc3QgVnVlIHRlc3RzIHBhc3MgdW5jaGFuZ2VkOyBhIGZldyBzcHkgYW5kIHRpbWVyIGhhYml0cyBkbyBub3Qu%2Ceyebrow_~UmVsZWFzZSBub3RlcyDCtyA2IG1pbiByZWFk%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy92aXRlc3QtMy13aGF0LWNoYW5nZXMtZm9yLXZ1ZS10ZXN0cyI%2Cs_mYKym78roAHz-wRr.png"}]}
```