Migration Guide
Migrating to Vitest 4.0
V8 Code Coverage Major Changes
Vitest's V8 code coverage provider is now using more accurate coverage result remapping logic. It is expected for users to see changes in their coverage reports when updating from Vitest v3.
In the past Vitest used v8-to-istanbul for remapping V8 coverage results into your source files. This method wasn't very accurate and provided plenty of false positives in the coverage reports. We've now developed a new package that utilizes AST based analysis for the V8 coverage. This allows V8 reports to be as accurate as @vitest/coverage-istanbul reports.
- Coverage ignore hints have updated. See Coverage | Ignoring Code.
coverage.ignoreEmptyLinesis removed. Lines without runtime code are no longer included in reports.coverage.experimentalAstAwareRemappingis removed. This option is now enabled by default, and is the only supported remapping method.coverage.ignoreClassMethodsis now supported by V8 provider too.
Removed options coverage.all and coverage.extensions
In previous versions Vitest included all uncovered files in coverage report by default. This was due to coverage.all defaulting to true, and coverage.include defaulting to **. These default values were chosen for a good reason - it is impossible for testing tools to guess where users are storing their source files.
This ended up having Vitest's coverage providers processing unexpected files, like minified Javascript, leading to slow/stuck coverage report generations. In Vitest v4 we have removed coverage.all completely and defaulted to include only covered files in the report.
When upgrading to v4 it is recommended to define coverage.include in your configuration, and then start applying simple coverage.exclude patterns if needed.
export default defineConfig({
test: {
coverage: {
// Include covered and uncovered files matching this pattern:
include: ['packages/**/src/**.{js,jsx,ts,tsx}'],
// Exclusion is applied for the files that match include pattern above
// No need to define root level *.config.ts files or node_modules, as we didn't add those in include
exclude: ['**/some-pattern/**'],
// These options are removed now
all: true,
extensions: ['js', 'ts'],
}
}
})If coverage.include is not defined, coverage report will include only files that were loaded during test run:
export default defineConfig({
test: {
coverage: {
// Include not set, include only files that are loaded during test run
include: undefined,
// Loaded files that match this pattern will be excluded:
exclude: ['**/some-pattern/**'],
}
}
})See also new guides:
- Including and excluding files from coverage report for examples
- Profiling Test Performance | Code coverage for tips about debugging coverage generation
spyOn and fn Support Constructors
Previously, if you tried to spy on a constructor with vi.spyOn, you would get an error like Constructor <name> requires 'new'. Since Vitest 4, all mocks called with a new keyword construct the instance instead of calling mock.apply. This means that the mock implementation has to use either the function or the class keyword in these cases:
const cart = {
Apples: class Apples {
getApples() {
return 42
}
}
}
const Spy = vi.spyOn(cart, 'Apples')
.mockImplementation(() => ({ getApples: () => 0 }))
// with a function keyword
.mockImplementation(function () {
this.getApples = () => 0
})
// with a custom class
.mockImplementation(class MockApples {
getApples() {
return 0
}
})
const mock = new Spy()Note that now if you provide an arrow function, you will get <anonymous> is not a constructor error when the mock is called.
Changes to Mocking
Alongside new features like supporting constructors, Vitest 4 creates mocks differently to address several module mocking issues that we received over the years. This release attempts to make module spies less confusing, especially when working with classes.
vi.fn().getMockName()now returnsvi.fn()by default instead ofspy. This can affect snapshots with mocks - the name will be changed from[MockFunction spy]to[MockFunction]. Spies created withvi.spyOnwill keep using the original name by default for better debugging experiencevi.restoreAllMocksno longer resets the state of spies and only restores spies created manually withvi.spyOn, automocks are no longer affected by this function (this also affects the config optionrestoreMocks). Note that.mockRestorewill still reset the mock implementation and clear the state- Calling
vi.spyOnon a mock now returns the same mock mock.settledResultsare now populated immediately on function invocation with an'incomplete'result. When the promise is finished, the type is changed according to the result.- Automocked instance methods are now properly isolated, but share a state with the prototype. Overriding the prototype implementation will always affect instance methods unless the methods have a custom mock implementation of their own. Calling
.mockReseton the mock also no longer breaks that inheritance.
import { AutoMockedClass } from './example.js'
const instance1 = new AutoMockedClass()
const instance2 = new AutoMockedClass()
instance1.method.mockReturnValue(42)
expect(instance1.method()).toBe(42)
expect(instance2.method()).toBe(undefined)
expect(AutoMockedClass.prototype.method).toHaveBeenCalledTimes(2)
instance1.method.mockReset()
AutoMockedClass.prototype.method.mockReturnValue(100)
expect(instance1.method()).toBe(100)
expect(instance2.method()).toBe(100)
expect(AutoMockedClass.prototype.method).toHaveBeenCalledTimes(4)- Automocked methods can no longer be restored, even with a manual
.mockRestore. Automocked modules withspy: truewill keep working as before - Automocked getters no longer call the original getter. By default, automocked getters now return
undefined. You can keep usingvi.spyOn(object, name, 'get')to spy on a getter and change its implementation - The mock
vi.fn(implementation).mockReset()now correctly returns the mock implementation in.getMockImplementation() vi.fn().mock.invocationCallOrdernow starts with1, like Jest does, instead of0
Standalone mode with filename filter
To improve user experience, Vitest will now start running the matched files when --standalone is used with filename filter.
# In Vitest v3 and below this command would ignore "math.test.ts" filename filter.
# In Vitest v4 the math.test.ts will run automatically.
$ vitest --standalone math.test.tsThis allows users to create re-usable package.json scripts for standalone mode.
{
"scripts": {
"test:dev": "vitest --standalone"
}
}# Start Vitest in standalone mode, without running any files on start
$ pnpm run test:dev
# Run math.test.ts immediately
$ pnpm run test:dev math.test.tsReplacing vite-node with Module Runner
Module Runner is a successor to vite-node implemented directly in Vite. Vitest now uses it directly instead of having a wrapper around Vite SSR handler. This means that certain features are no longer available:
VITE_NODE_DEPS_MODULE_DIRECTORIESenvironment variable was replaced withVITEST_MODULE_DIRECTORIES- Vitest no longer injects
__vitest_executorinto every test runner. Instead, it injectsmoduleRunnerwhich is an instance ofModuleRunner vitest/executeentry point was removed. It was always meant to be internal- Custom environments no longer need to provide a
transformModeproperty. Instead, provideviteEnvironment. If it is not provided, Vitest will use the environment name to transform files on the server (seeserver.environments) vite-nodeis no longer a dependency of Vitestdeps.optimizer.webwas renamed todeps.optimizer.client. You can also use any custom names to apply optimizer configs when using other server environments
Vite has its own externalization mechanism, but we decided to keep using the old one to reduce the amount of breaking changes. You can keep using server.deps to inline or externalize packages.
This update should not be noticeable unless you rely on advanced features mentioned above.
workspace is Replaced with projects
The workspace configuration option was renamed to projects in Vitest 3.2. They are functionally the same, except you cannot specify another file as the source of your workspace (previously you could specify a file that would export an array of projects). Migrating to projects is easy, just move the code from vitest.workspace.js to vitest.config.ts:
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
workspace: './vitest.workspace.js',
projects: [
'./packages/*',
{
test: {
name: 'unit',
},
},
]
}
})import { defineWorkspace } from 'vitest/config'
export default defineWorkspace([
'./packages/*',
{
test: {
name: 'unit',
},
}
]) Browser Provider Rework
In Vitest 4.0, the browser provider now accepts an object instead of a string ('playwright', 'webdriverio'). The preview is no longer a default. This makes it simpler to work with custom options and doesn't require adding /// <reference comments anymore.
import { playwright } from '@vitest/browser-playwright'
export default defineConfig({
test: {
browser: {
provider: 'playwright',
provider: playwright({
launchOptions: {
slowMo: 100,
},
}),
instances: [
{
browser: 'chromium',
launch: {
slowMo: 100,
},
},
],
},
},
})The naming of properties in playwright factory now also aligns with Playwright documentation making it easier to find.
With this change, the @vitest/browser package is no longer needed, and you can remove it from your dependencies. To support the context import, you should update the @vitest/browser/context to vitest/browser:
import { page } from '@vitest/browser/context'
import { page } from 'vitest/browser'
test('example', async () => {
await page.getByRole('button').click()
})The modules are identical, so doing a simple "Find and Replace" should be sufficient.
If you were using the @vitest/browser/utils module, you can now import those utilities from vitest/browser as well:
import { getElementError } from '@vitest/browser/utils'
import { utils } from 'vitest/browser'
const { getElementError } = utils WARNING
Both @vitest/browser/context and @vitest/browser/utils work at runtime during the transition period, but they will be removed in a future release.
Pool Rework
Vitest has used tinypool for orchestrating how test files are run in the test runner workers. Tinypool has controlled how complex tasks like parallelism, isolation and IPC communication works internally. However we've found that Tinypool has some flaws that are slowing down development of Vitest. In Vitest v4 we've completely removed Tinypool and rewritten how pools work without new dependencies. Read more about reasoning from feat!: rewrite pools without tinypool #8705 .
New pool architecture allows Vitest to simplify many previously complex configuration options:
maxThreadsandmaxForksare nowmaxWorkers.- Environment variables
VITEST_MAX_THREADSandVITEST_MAX_FORKSare nowVITEST_MAX_WORKERS. singleThreadandsingleForkare nowmaxWorkers: 1, isolate: false. If your tests were relying on module reset between tests, you'll need to add setupFile that callsvi.resetModules()inbeforeAlltest hook.poolOptionsis removed. All previouspoolOptionsare now top-level options. ThememoryLimitof VM pools is renamed tovmMemoryLimit.threads.useAtomicsis removed. If you have a use case for this, feel free to open a new feature request.- Custom pool interface has been rewritten, see Custom Pool
export default defineConfig({
test: {
poolOptions: {
forks: {
execArgv: ['--expose-gc'],
isolate: false,
singleFork: true,
},
vmThreads: {
memoryLimit: '300Mb'
},
},
execArgv: ['--expose-gc'],
isolate: false,
maxWorkers: 1,
vmMemoryLimit: '300Mb',
}
})Previously it was not possible to specify some pool related options per project when using Vitest Projects. With the new architecture this is no longer a blocker.
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
projects: [
{
// Non-isolated unit tests
name: 'Unit tests',
isolate: false,
exclude: ['**.integration.test.ts'],
},
{
// Isolated integration tests
name: 'Integration tests',
include: ['**.integration.test.ts'],
},
],
},
})import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
projects: [
{
name: 'Parallel',
exclude: ['**.sequantial.test.ts'],
},
{
name: 'Sequential',
include: ['**.sequantial.test.ts'],
fileParallelism: false,
},
],
},
})import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
projects: [
{
name: 'Production env',
execArgv: ['--env-file=.env.prod']
},
{
name: 'Staging env',
execArgv: ['--env-file=.env.staging']
},
],
},
})See Recipes for more examples.
Reporter Updates
Reporter APIs onCollected, onSpecsCollected, onPathsCollected, onTaskUpdate and onFinished were removed. See Reporters API for new alternatives. The new APIs were introduced in Vitest v3.0.0.
The basic reporter was removed as it is equal to:
export default defineConfig({
test: {
reporters: [
['default', { summary: false }]
]
}
})The verbose reporter now prints test cases as a flat list. To revert to the previous behaviour, use --reporter=tree:
export default defineConfig({
test: {
reporters: ['verbose'],
reporters: ['tree'],
}
})Snapshots using custom elements print the shadow root
In Vitest 4.0 snapshots that include custom elements will print the shadow root contents. To restore the previous behavior, set the printShadowRoot option to false.
// before Vite 4.0
exports[`custom element with shadow root 1`] = `
"<body>
<div>
<custom-element />
</div>
</body>"
`
// after Vite 4.0
exports[`custom element with shadow root 1`] = `
"<body>
<div>
<custom-element>
#shadow-root
<span
class="some-name"
data-test-id="33"
id="5"
>
hello
</span>
</custom-element>
</div>
</body>"
`Deprecated APIs are Removed
Vitest 4.0 removes some deprecated APIs, including:
poolMatchGlobsconfig option. Useprojectsinstead.environmentMatchGlobsconfig option. Useprojectsinstead.deps.external,deps.inline,deps.fallbackCJSconfig options. Useserver.deps.external,server.deps.inline, orserver.deps.fallbackCJSinstead.browser.testerScriptsconfig option. Usebrowser.testerHtmlPathinstead.minWorkersconfig option. OnlymaxWorkershas any effect on how tests are running, so we are removing this public option.- Vitest no longer supports providing test options object as a third argument to
testanddescribe. Use the second argument instead:
test('example', () => { /* ... */ }, { retry: 2 })
test('example', { retry: 2 }, () => { /* ... */ }) Note that providing a timeout number as the last argument is still supported:
test('example', () => { /* ... */ }, 1000) // ✅This release also removes all deprecated types. This finally fixes an issue where Vitest accidentally pulled in @types/node (see #5481 and #6141).
Migrating from Jest
Vitest has been designed with a Jest compatible API, in order to make the migration from Jest as simple as possible. Despite those efforts, you may still run into the following differences:
Globals as a Default
Jest has their globals API enabled by default. Vitest does not. You can either enable globals via the globals configuration setting or update your code to use imports from the vitest module instead.
If you decide to keep globals disabled, be aware that common libraries like testing-library will not run auto DOM cleanup.
mock.mockReset
Jest's mockReset replaces the mock implementation with an empty function that returns undefined.
Vitest's mockReset resets the mock implementation to its original. That is, resetting a mock created by vi.fn(impl) will reset the mock implementation to impl.
mock.mock is Persistent
Jest will recreate the mock state when .mockClear is called, meaning you always need to access it as a getter. Vitest, on the other hand, holds a persistent reference to the state, meaning you can reuse it:
const mock = vi.fn()
const state = mock.mock
mock.mockClear()
expect(state).toBe(mock.mock) // fails in JestModule Mocks
When mocking a module in Jest, the factory argument's return value is the default export. In Vitest, the factory argument has to return an object with each export explicitly defined. For example, the following jest.mock would have to be updated as follows:
jest.mock('./some-path', () => 'hello')
vi.mock('./some-path', () => ({
default: 'hello',
})) For more details please refer to the vi.mock api section.
Auto-Mocking Behaviour
Unlike Jest, mocked modules in <root>/__mocks__ are not loaded unless vi.mock() is called. If you need them to be mocked in every test, like in Jest, you can mock them inside setupFiles.
Importing the Original of a Mocked Package
If you are only partially mocking a package, you might have previously used Jest's function requireActual. In Vitest, you should replace these calls with vi.importActual.
const { cloneDeep } = jest.requireActual('lodash/cloneDeep')
const { cloneDeep } = await vi.importActual('lodash/cloneDeep') Extends mocking to external libraries
Where Jest does it by default, when mocking a module and wanting this mocking to be extended to other external libraries that use the same module, you should explicitly tell which 3rd-party library you want to be mocked, so the external library would be part of your source code, by using server.deps.inline.
server.deps.inline: ["lib-name"]expect.getState().currentTestName
Vitest's test names are joined with a > symbol to make it easier to distinguish tests from suites, while Jest uses an empty space ().
- `${describeTitle} ${testTitle}`
+ `${describeTitle} > ${testTitle}`Envs
Just like Jest, Vitest sets NODE_ENV to test, if it wasn't set before. Vitest also has a counterpart for JEST_WORKER_ID called VITEST_POOL_ID (always less than or equal to maxWorkers), so if you rely on it, don't forget to rename it. Vitest also exposes VITEST_WORKER_ID which is a unique ID of a running worker - this number is not affected by maxWorkers, and will increase with each created worker.
Replace property
If you want to modify the object, you will use replaceProperty API in Jest, you can use vi.stubEnv or vi.spyOn to do the same also in Vitest.
Done Callback
Vitest does not support the callback style of declaring tests. You can rewrite them to use async/await functions, or use Promise to mimic the callback style.
it('should work', (done) => {
it('should work', () => new Promise(done => {
// ...
done()
})
})) Hooks
beforeAll/beforeEach hooks may return teardown function in Vitest. Because of that you may need to rewrite your hooks declarations, if they return something other than undefined or null:
beforeEach(() => setActivePinia(createTestingPinia()))
beforeEach(() => { setActivePinia(createTestingPinia()) }) In Jest hooks are called sequentially (one after another). By default, Vitest runs hooks in a stack. To use Jest's behavior, update sequence.hooks option:
export default defineConfig({
test: {
sequence: {
hooks: 'list',
}
}
})Types
Vitest doesn't have an equivalent to jest namespace, so you will need to import types directly from vitest:
let fn: jest.Mock<(name: string) => number>
import type { Mock } from 'vitest'
let fn: Mock<(name: string) => number> Timers
Vitest doesn't support Jest's legacy timers.
Timeout
If you used jest.setTimeout, you would need to migrate to vi.setConfig:
jest.setTimeout(5_000)
vi.setConfig({ testTimeout: 5_000 }) Vue Snapshots
This is not a Jest-specific feature, but if you previously were using Jest with vue-cli preset, you will need to install jest-serializer-vue package, and specify it in snapshotSerializers:
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
snapshotSerializers: ['jest-serializer-vue']
}
})Otherwise your snapshots will have a lot of escaped " characters.