迁移到 Vitest 5.0
迁移到 Vitest 4.0 | 迁移到 Vitest 3.0
前置条件
Vitest 5.0 需要 Vite >= 6.4.0 和 Node.js >= 22.12.0。进行其他迁移步骤前,请确认环境符合这些要求。不支持在旧版本的 Vite 或 Node.js 上运行 Vitest 5.0,否则可能出现意外错误。
Yarn 用户必须显式安装 vite
vite 不再是 vitest 的直接依赖,现在是必需的 peer dependency,因此 Vitest 会使用项目中安装的 Vite 版本。npm、pnpm、Bun 和 Deno 会自动安装 peer dependencies,Yarn 则不会。因此升级后,除非在 package.json 中列出 vite,否则 Vitest 无法解析它:
yarn add -D viteclearMocks 默认启用
clearMocks 现在默认为 true:Vitest 会在每个测试前调用 vi.clearAllMocks(),清除每个 mock 的调用历史,同时保留其实现。
这意味着一个 mock 不会再把某个测试中的调用记录带到下一个测试中:
import { expect, test, vi } from 'vitest'
const fn = vi.fn()
test('first', () => {
fn()
expect(fn).toHaveBeenCalledTimes(1)
})
test('second', () => {
fn()
// v4: the call from "first" was kept, so this was 2
expect(fn).toHaveBeenCalledTimes(2)
// v5: history is cleared before each test, so only this test's call counts
expect(fn).toHaveBeenCalledTimes(1)
})在测试主体之外记录调用的测试受影响最大,例如在 setup 文件、模块顶层或 beforeAll 钩子中记录的调用,因为在断言它们的测试运行前,这些历史记录就会被清除。
如需恢复之前的行为,将 clearMocks 重新设为 false:
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
clearMocks: false,
},
})testNamePattern 匹配以 > 连接的完整名称
testNamePattern(-t CLI 标志)现在会匹配测试的完整名称,其中套件层级与测试名称通过 ' > ' 连接,与报告器输出中显示的字符串相同。此前各段之间使用单个空格连接,与 Jest 保持一致。
只有跨越两个名称段边界的模式会受到影响:
describe('math', () => {
test('adds', () => {})
})vitest -t 'math adds'
vitest -t 'math > adds'要让模式不受分隔符影响,可以匹配单个名称段(-t adds),或在各段之间使用通配符(-t 'math.*adds')。
内联项目默认继承根配置
extends 选项现在默认为 true:在 test.projects 中以内联配置定义的每个项目都会继承根配置中的所有选项,包括 plugins 或 resolve.alias 等 Vite 选项。选项合并规则与 Vitest 4 中显式设置 extends: true 时相同:
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
test: {
projects: [
{
// v4: this project didn't apply the react plugin
// v5: the plugin is inherited from the root config
test: {
name: 'unit',
include: ['**/*.unit.test.ts'],
},
},
],
},
})作为配置文件或目录引用的项目不受影响;它们仍不会继承根配置中的任何选项。
请注意,数组会合并而不是覆盖:如果根配置定义了 setupFiles,项目自己的 setupFiles 会追加到继承的值之后。有关合并规则和少数永远不会继承的选项,请参阅项目指南。如需恢复之前的行为,请在项目配置中设置 extends: false:
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
setupFiles: ['./setup.global.ts'],
projects: [
{
extends: false,
test: {
name: 'unit',
setupFiles: ['./setup.unit.ts'],
},
},
],
},
})被引用的配置文件可以定义自己的项目
在 test.projects 中引用且自身声明了 projects 的配置文件,现在会提供它声明的嵌套项目(例如 app (unit)、app (e2e)),而不是将测试作为单个项目运行。
在 Vitest 4 中,被引用配置的 projects 字段会被静默忽略。请检查项目配置中是否意外带有 projects 字段,最常见的情况是合并了一个定义该字段的配置:
import { defineProject, mergeConfig } from 'vitest/config'
import rootConfig from '../../vitest.config'
import sharedConfig from '../../vitest.shared'
export default mergeConfig(
// the root config defines `test.projects`, so merging it
// would turn this project into a container for those projects
rootConfig,
sharedConfig,
defineProject({
test: {
environment: 'jsdom',
},
}),
)由于继承的 projects 路径会相对于被引用的配置进行解析,这种错误配置通常会在启动时明确报错,例如 Projects definition references a non-existing file or a directory、No projects were found in "...",或循环定义 projects。
内联配置在运行时仍会忽略 projects 字段,但该字段现在也已从其 ProjectConfig 类型中排除。
内联项目默认共享 Vite 服务器
不修改 Vite 配置的内联项目现在会复用声明它们的配置所使用的 Vite 服务器,而不是为每个项目解析新的 Vite 配置并创建新服务器。这样共享文件只需转换一次,测试运行也更快。此行为由新的 sharedViteServer 选项控制,该选项默认启用;相关文档列出了仍会让项目获得独立服务器的具体选项。
请注意,这_仅_适用于内联项目。作为配置文件或目录引用的项目始终会解析自己的 Vite 配置并创建自己的服务器,与之前完全相同。
可观察到的变化是:共享服务器时,声明配置文件只执行一次,而不是每个项目执行一次,因此其中的插件也只实例化一次,其 config 钩子不再为每个项目分别运行。如果插件依赖于为每个项目重新实例化,请禁用服务器共享:
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
sharedViteServer: false,
projects: [
{ test: { name: 'unit' } },
{ test: { name: 'integration' } },
],
},
})提升的模拟调用必须位于顶层
vi.mock、vi.unmock 和 vi.hoisted 会被提升到文件顶部,并在任何外围代码之前执行。此前在函数、代码块或 describe/test 回调中调用它们只会记录警告。Vitest 5.0 现在会抛出错误,因为这些调用并不会在其书写位置执行:
describe('calculator', () => {
vi.mock('./calculator')
})
vi.mock('./calculator')
describe('calculator', () => {
// ...
})错误会报告每个违规调用及其位置:
1 call in "calculator.test.ts" was defined outside of the module's top level scope:
- vi.mock("./calculator") at calculator.test.ts:2:3
Although it appears nested, it will be hoisted and executed before anything in this file. Move it to the top level to reflect its actual execution order.动态版本 vi.doMock 和 vi.doUnmock 不会被提升,仍可在任意位置调用。
浏览器中的模块仍会自动模拟
在浏览器模式下,自动模拟模块的导出(即不带工厂函数的 vi.mock 调用)此前错误地继续调用真实实现,而不是自动生成的存根。如果浏览器测试依赖此行为,现在其导出默认会返回 undefined。传入 { spy: true } 可以在跟踪调用的同时继续调用真实实现,也可以提供包含所需行为的工厂函数。
类 Mock 会保留原型方法
此前,通过类 mock 创建的实例会继承 mock 自身的空 prototype。使用常规 class 语法定义的方法在实例上会是 undefined,即使是在构造函数内部也是如此;针对实现类的 instanceof 检查也会失败。这会影响 vi.fn(Dog)、带或不带 mock 实现的 vi.spyOn(obj, 'Dog'),以及 .mockImplementation(class ...)。
现在,mock 的 prototype 会链接到实现类的原型,因此实例的行为与实现类的实例一致:
class Dog {
speak() {
return 'bark!'
}
}
const MockedDog = vi.fn(Dog)
const dog = new MockedDog()
typeof dog.speak // was 'undefined', now 'function'
dog instanceof Dog // was false, now true
dog instanceof MockedDog // true, as before
// the chain is visible on the mock itself
Object.getPrototypeOf(MockedDog.prototype) // was Object.prototype, now Dog.prototype仍然可以覆盖 mock prototype 上的方法,这些方法会遮蔽实现类中的对应方法;mockReset 会连同实现一起恢复原型链。详情请参阅模拟类。
基准测试 API 重写
基准测试 API 已重写。bench 不再是从 vitest 顶层导入的内容,而是一个测试上下文 fixture,需要在普通的 test() 内访问。新 API 请参阅基准测试指南。
以下内容已移除,并在适用时提供替代方案:
- 模块作用域下的
bench(name, fn):改为从测试上下文中解构bench。
// v4
import { bench } from 'vitest'
bench('sort', () => {
[3, 1, 2].sort()
})
// v5
import { test } from 'vitest'
test('sort', async ({ bench }) => {
await bench('sort', () => { [3, 1, 2].sort() }).run()
}) bench.skip、bench.only和bench.todo已移除。请改为在外围的test()上使用常规的test.skip、test.only和test.todo。benchmark.reporters/benchmark.outputFile已移除。基准测试输出现在由默认报告器和json报告器提供;请通过顶层的test.reporters配置。benchmark.compare配置和--compareCLI 标志 已移除。将writeResult作为单个基准测试的选项传入以保存结果,再通过bench.compare()中的bench.from()读取。benchmark.outputJson配置和--outputJsonCLI 标志 已移除。使用--reporter=json --outputFile=<path>捕获基准测试结果;JSON 报告器现在会在每个测试用例中包含benchmarks字段。Vitest实例的mode属性现在始终为'test'。不再使用之前的'benchmark'值;基准测试会在同一个Vitest实例的专用项目中运行。
Vitest UI URL 需要身份验证
Vitest UI 现在要求通过 token 验证 HTML 页面和 API 访问权限。在浏览器完成身份验证前,访问 /__vitest__/ 会显示错误。要进行身份验证,请像下面这样使用 Vitest 输出的 token 打开 URL。完成验证后,直接访问 /__vitest__/ 即可正常使用。
vitest --ui
# UI started at http://localhost:51204/__vitest__/?token=...假计时器和 setSystemTime 现在会模拟 Temporal
根据 @sinonjs/fake-timers v15.4 更新,Vitest 现在会像模拟 Date 一样模拟 Temporal API。只有当 Temporal 可通过原生实现或全局安装的 polyfill(例如 import 'temporal-polyfill/global')存在于全局对象上时,此功能才会生效。
此前,即使启用了 vi.useFakeTimers(),Temporal.Now 仍会返回真实的当前时间。现在它会跟随模拟时钟:
vi.useFakeTimers({ now: 0 })
Temporal.Now.instant().epochMilliseconds // 0 (was the real time in v4)vi.setSystemTime() 也是如此;此前不配合假计时器使用时,它只会模拟 Date:
vi.setSystemTime(0)
Temporal.Now.instant().epochMilliseconds // 0 (was the real time in v4)Temporal 现在属于默认模拟的 API,因此受 fakeTimers.toFake 和 fakeTimers.toNotFake 控制。如需保留原生的 Temporal,请将它添加到 toNotFake:
vi.useFakeTimers({ toNotFake: ['Temporal'] })toThrow("") 匹配任意错误消息
toThrow(及其别名 toThrowError)会将字符串参数视为错误消息的子字符串。在 Vitest 4 中,空字符串被特殊处理为 /^$/ 模式,因此只匹配消息为空的错误。现在它与其他子字符串的行为一致,而空字符串包含于所有消息中:
expect(() => { throw new Error('boom') }).not.toThrow('')
expect(() => { throw new Error('boom') }).toThrow('') 要断言抛出的错误消息为空,请显式匹配该模式:
expect(() => { throw new Error('boom') }).not.toThrow(/^$/)断言类型会公开返回类型和接收值类型
断言接口现在使用两个类型参数:R 是匹配器的返回类型,T 是接收值的类型。同步断言使用 void,而通过 .resolves、.rejects、expect.poll 或 expect.element 访问的断言使用 Promise<void>。
如果你声明了自定义匹配器,请像扩展匹配器中那样扩展 Matchers<R, T> 接口。这样会将匹配器添加到实例断言、非对称匹配器和 expect.extend 接受的类型中。匹配器返回类型会反映其使用方式:同步调用时为 void,通过 .resolves 或 .rejects 调用时为 Promise<void>。
直接引用断言类型的代码也必须先提供返回类型:
Assertion<string>
Assertion<void, string>
Assertion<Promise<void>, string> // asynchronous assertionVitest 不再从全局 jest.Matchers 接口读取自定义匹配器声明。若库同时支持 Jest 和 Vitest,应分别扩展 jest.Matchers 与 vitest.Matchers。这只影响 TypeScript 声明;使用 expect.extend 注册匹配器的方式不变。
expect.poll 超时后会失败
如果 expect.poll 的回调或轮询中的断言未能在 timeout 内完成,现在会以 reject 结束。此前,即使回调在截止时间后才解析,或断言只在较晚的一次尝试中通过,仍可能成功。现在回调还会收到一个 AbortSignal,并在超时时触发,以便取消正在进行的工作:
await expect.poll(async ({ signal }) => {
const response = await fetch('/api/status', { signal })
return response.status
}, { timeout: 1000 }).toBe(200)如果轮询确实需要更多时间,应增大其 timeout。否则会以 expect.poll() function didn't resolve in time.(或 expect.poll() assertion didn't resolve in time.)错误失败。
未 await 的异步断言会导致测试失败
如果没有对异步断言执行 await,现在测试会失败。这类断言包括 resolves、rejects 和 toMatchFileSnapshot。此前 Vitest 会在测试结束时自动等待它们,并打印警告:
test('unawaited assertion', async () => {
// v4: prints a warning, the test passes
// v5: the test fails
expect(promise).resolves.toBe(1)
await expect(promise).resolves.toBe(1)
})错误会指出未执行 await 的断言位置。
测试标题和检查值使用 pretty-format
Vitest 现在使用 pretty-format 而不是 loupe 来格式化检查值,包括插入 test.each 和 test.for 标题中的值。部分值的呈现方式会改变,因此捕获检查结果的快照或断言可能需要更新。
测试标题生成方式有两项变化:
- 通过
$占位符插入的字符串值不再带引号:
test.for([{ id: 'a1' }])('case $id', ({ id }) => { /* ... */ })
// v4 title: case 'a1'
// v5 title: case a1- 插入值的长度限制现在由新的
taskTitleValueFormatTruncate选项控制(默认值为40)。
移除了 test.sequential、describe.sequential 和 sequential 选项
Vitest 5.0 移除了已弃用的 test.sequential、describe.sequential 和 sequential 测试选项。如果需要让测试或套件退出继承或全局配置的并发模式,请使用 concurrent: false。
test.sequential('example', async () => { /* ... */ })
test('example', { concurrent: false }, async () => { /* ... */ }) describe.sequential('suite', () => { /* ... */ })
describe('suite', { concurrent: false }, () => { /* ... */ }) 选项对象也需要进行相同的替换:
test('example', { sequential: true }, async () => { /* ... */ })
test('example', { concurrent: false }, async () => { /* ... */ }) 命令中的定位器会序列化为对象
传递给浏览器命令的定位器现在会序列化为 SerializedLocator 对象,而不是单独的选择器字符串。该对象公开两个字段:
selector:提供者专属的选择器字符串(与命令之前收到的值相同)。locator:定位器的可读表示形式(例如getByRole('button')),用于错误消息和追踪。
如果自定义命令接收定位器,请更新代码,从新对象中解构 selector:
import type { SerializedLocator } from '@vitest/browser'
import type { BrowserCommandContext } from 'vitest/node'
export async function customClick(
context: BrowserCommandContext,
selector: string,
{ selector }: SerializedLocator,
) {
await context.page.locator(selector).click()
}定位器默认使用严格匹配
浏览器定位器现在默认精确匹配文本,要求完整且区分大小写的匹配。如需保留之前的行为,可以将 browser.locators.exact 设为 false。
// With exact: true (default), this only matches the string "Hello, World" exactly.
// With exact: false, this matches "Hello, World!", "Say Hello, World", etc.
const locator = page.getByText('Hello, World', { exact: true })
await locator.click()toHaveTextContent 现在使用严格相等匹配
浏览器模式中的 toHaveTextContent 匹配器现在会验证元素文本内容是否与预期字符串完全相等,而不是执行区分大小写的部分匹配。它不再接受正则表达式。此前的行为(包括 RegExp 支持)已移至新的 toMatchTextContent 匹配器。
// Partial or regex matches:
await expect.element(banner).toHaveTextContent('Error')
await expect.element(banner).toHaveTextContent(/error/i)
await expect.element(banner).toMatchTextContent('Error')
await expect.element(banner).toMatchTextContent(/error/i)
// Exact matches stay on `toHaveTextContent`:
await expect.element(banner).toHaveTextContent('Error!')vitest-browser-vue 和 vitest-browser-svelte 中的 render 是异步的
配套的组件测试包 vitest-browser-vue 和 vitest-browser-svelte 现在会让 render 返回 promise,因此查询渲染结果之前必须等待该调用完成:
import { render } from 'vitest-browser-vue'
import Component from './Component.vue'
test('renders', async () => {
const screen = render(Component)
const screen = await render(Component)
await expect.element(screen.getByRole('heading')).toBeVisible()
})glob 覆盖率阈值不再继承 perFile
此前,coverage.thresholds.perFile 会应用到所有阈值设置,包括由 glob 模式阈值匹配的文件。现在 glob 模式会自行控制逐文件检查,不再继承顶层 perFile。需要逐文件检查时,请在对应的每个 glob 中设置 perFile。
export default defineConfig({
test: {
coverage: {
thresholds: {
'perFile': true,
'src/utils/**': {
lines: 80,
perFile: true,
},
},
},
},
})覆盖率 include 和 exclude 匹配更精确
coverage.include 和 coverage.exclude 之前会使用 picomatch 的 contains 选项匹配绝对路径,因此匹配到的文件远多于预期。现在模式会在不使用 contains 的情况下,针对每个相对于项目根目录的文件路径进行匹配;不含 glob 通配符的模式会被视为目录,并匹配其中的所有内容:
export default defineConfig({
test: {
coverage: {
include: ['src'], // matches src/**, not every path that contains "src"
},
},
})升级后请检查 include 和 exclude 模式,确认报告的文件集合符合预期。之前仅因匹配规则较宽松而被匹配的文件,现在可能不会再包含在内。
不再从父目录查找配置文件
Vitest 不再从父目录搜索配置文件。如果你之前依赖在子目录中运行 vitest、同时使用父目录中的配置文件,请显式传入配置文件,并通过 --dir 限定测试发现范围。例如:
$ cd subdir && vitest
$ cd subdir && vitest --config ../vitest.config.tsDOM 环境中的全局赋值现在会更新底层 window
在 jsdom 和 happy-dom 环境中,对 globalThis 或 window 属性的赋值现在会传递到底层 DOM 实现。innerWidth 等可变属性可能会影响由 DOM 环境实现的 API,例如 happy-dom 的 matchMedia。
populateGlobal 在 originals 中返回属性描述符
populateGlobal 返回的 originals map 现在保存属性描述符,而非普通值。这样在捕获原始值时不会调用原生的惰性 getter(例如 Node 的 localStorage),并且能在 teardown 时准确恢复它们。
如果你在自定义环境中手动恢复它们,请使用 Object.defineProperty,而不是赋值:
originals.forEach((value, key) => (global[key] = value))
originals.forEach((descriptor, key) => Object.defineProperty(global, key, descriptor)) 浏览器编排器 URL 需要 session
Vitest 不再通过不带参数的 /__vitest_test__/ URL 提供浏览器编排器 UI。浏览器运行器 URL 现在与 session 绑定,必须包含 Vitest 生成的 sessionId,例如 /__vitest_test__/?sessionId=...。
如果你之前通过复制 Vite 服务器 URL 或直接访问 /__vitest_test__/ 手动打开浏览器预览,请改用 Vitest 打开或输出的 URL。
browser.api 已被顶层 api 替代
浏览器模式现在运行在单个 Vite 服务器上,并由顶层 api 选项配置;浏览器模式的默认端口仍为 63315。browser.api 选项已弃用且不再生效,请将其值移至 api:
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
api: { port: 4444 },
browser: {
enabled: true,
api: { port: 4444 },
},
},
})此前已弃用的 browser.isolate 选项现在也会在启动时打印警告;其值仍会应用到用于替代它的顶层 isolate 选项。
生成的报告和产物使用 .vitest 目录
Vitest 现在使用项目根目录下的 .vitest 作为共享产物根目录,因此只需在 .gitignore 中添加一条 .vitest。本次大版本调整了以下默认路径:
- 附件 (
attachmentsDir):.vitest-attachements/→.vitest/attachments/ - 失败截图 (
screenshotFailures):__screenshots__/→.vitest/attachments/failure-screenshots/,不再与toMatchScreenshot的参考截图混在一起 - Blob 报告器和
--merge-reports:.vitest-reports/blob-*.json→.vitest/blob/blob-*.json - HTML 报告器 (
html):html/index.html→.vitest/index.html,其选项也从表示文件的outputFile改为表示目录的outputDir - JSON 报告器 (
json):stdout →.vitest/json/output.json - JUnit 报告器 (
junit):stdout →.vitest/junit/output.xml
json 和 junit 报告器现在默认写入文件,而不是打印到 stdout。如果你之前通过管道处理报告(例如 vitest --reporter=json | jq),请改为读取产物文件;也可以通过报告器的 stdout 选项恢复输出到 stdout(reporters: [['json', { stdout: true }]])。显式设置的 outputFile 仍会保留且不变。
toMatchScreenshot 现在使用专用的截图目录配置
此前,toMatchScreenshot 的参考截图不会正确遵循 browser.screenshotDirectory。因此,配置自定义目录时,截图会保存到非预期的位置。
现在引入了专用选项以修复此问题:browser.expect.toMatchScreenshot.screenshotDirectory。其默认值为 __screenshots__。
如果你没有设置
browser.screenshotDirectory,则无需更改。如果设置了
browser.screenshotDirectory,现在必须显式配置新选项:tsexport default defineConfig({ test: { browser: { screenshotDirectory: 'my-screenshots', expect: { toMatchScreenshot: { screenshotDirectory: 'my-screenshots', }, }, }, }, })然后将现有参考截图移动到新位置,或重新生成它们。
Worker 和并发 ID 从 1 开始
Worker 和池标识符现在从 1 而不是 0 开始。VITEST_POOL_ID 和 VITEST_WORKER_ID 环境变量的值也随之变化,现在范围为 1 到 worker 数量。请更新任何根据这些 ID 计算值的逻辑,例如每个 worker 使用的数据库名称或数组索引。
对于自定义报告器,TestModule 的诊断信息现在会公开这两个 ID:现有的 workerId(现在从 1 开始)和新的 concurrencyId。
import type { Reporter, TestModule } from 'vitest/node'
class MyReporter implements Reporter {
onTestModuleEnd(testModule: TestModule) {
const { workerId, concurrencyId } = testModule.diagnostic()
}
}Node.js 和浏览器测试运行在不同的池中,不会共享这些 ID,因此两边可能出现相同的值。
resolveConfig 返回解析后的 Vite 配置
vitest/node 中的 resolveConfig 辅助函数不再返回 { vitestConfig, viteConfig } 对。它会在不创建 Vite 服务器的情况下解析配置并返回解析后的 Vite 配置;完整解析后的 Vitest 配置可通过其 test 属性获取:
import { resolveConfig } from 'vitest/node'
const { viteConfig, vitestConfig } = await resolveConfig(options)
const viteConfig = await resolveConfig(options)
const vitestConfig = viteConfig.test Vitest 4 中的限制已解除:返回的配置现在包含完整解析后的 projects,并且 viteConfig.test 不再包含仅部分解析的选项。
包迁移
以下包从此版本起弃用。它们将不再接收功能更新,但仍会回移安全修复:
vitest 也不再依赖 @vitest/expect:断言代码现已打包进 vitest 本身。该包仍会发布且可独立使用,但不再与 Vitest 的 expect 共享状态。请改为通过 vitest 入口(expect、expect.extend、chai)使用 Vitest 断言。
@vitest/browser-webdriverio 提供者已迁移至 vitest-community 组织。今后 WebdriverIO 支持将由社区维护,并按具体问题逐一处理。如果你正在使用它,请将依赖更新为新包,并在新的代码仓库中报告问题。
移除已弃用的入口
Vitest 4.1 中有多个入口被标记为弃用,本版本将其完全移除。
vitest/coverage:请改用vitest/nodevitest/reporters:请改用vitest/nodevitest/environments:请改用vitest/runtimevitest/snapshot:请改用vitest/runtimevitest/runners:请改为从vitest使用TestRunnervitest/suite:请改为使用vitest中TestRunner的静态方法(例如TestRunner.getCurrentTest())vitest/mocker已完全移除,请直接使用@vitest/mocker包(该包曾被意外发布,之后一直未被移除)vitest/internal/module-runner已移除