迁移指南
迁移到 Vitest 4.0 | 迁移到 Vitest 3.0。
迁移到 Vitest 5.0
Prerequisites
Vitest 5.0 requires Vite >= 6.4.0 and Node.js >= 22.12.0. Before proceeding with any other migration steps, ensure your environment meets these requirements. Running Vitest 5.0 on older versions of Vite or Node.js is not supported and may result in unexpected errors.
clearMocks 默认已启用
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: 来自 "first" 的调用被保留了,所以这里是 2
expect(fn).toHaveBeenCalledTimes(2)
// v5: 每个测试前都会清空历史,因此这里只计算这个测试中的调用
expect(fn).toHaveBeenCalledTimes(1)
})在测试主体之外记录调用的测试(例如在 setup 文件中、模块顶层,或者在 beforeAll 钩子中)受影响最大,因为在执行断言的测试运行之前,这些历史会被清空。
如果要保持之前的行为,将 clearMocks 重新设置为 false:
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
clearMocks: false,
},
})testNamePattern 匹配由 > 连接的完整名称
testNamePattern(-t CLI 标志)现在会匹配测试的完整名称,其中测试套件链和测试名称通过 ' > ' 连接,这与 reporter 输出中显示的字符串相同。此前各段之间使用单个空格连接,与 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:此项目不会应用 react 插件
// v5:此插件会从根配置继承
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(
// 根配置定义了 `test.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' } },
],
},
})提升的 Mock 调用必须位于顶层
vi.mock、vi.unmock 和 vi.hoisted 会被提升到文件顶部,并在任何外围代码之前执行。之前在函数、代码块或 describe/test 回调中调用它们只会记录警告。Vitest 5.0 现在会抛出错误,因为该调用并不会在其书写位置执行:
describe('calculator', () => {
vi.mock('./calculator')
})
vi.mock('./calculator')
describe('calculator', () => {
// ...
})错误会报告每个违规调用及其位置:
"calculator.test.ts" 中有 1 个调用定义在模块顶层作用域之外:
- vi.mock("./calculator") 位于 calculator.test.ts:2:3
尽管它看起来是嵌套的,但它会被提升并在本文件中的任何内容之前执行。请将其移至顶层,以反映其实际执行顺序。动态变体 vi.doMock 和 vi.doUnmock 不会被提升,仍然可以在任何位置调用。
自动 mock 模块在浏览器中仍保持自动 mock
在浏览器模式下,自动 mock 模块的导出内容(即不带工厂函数的 vi.mock 调用)错误地仍会调用真实实现,而不是自动生成的存根。如果浏览器测试依赖于该行为,其导出内容现在默认会返回 undefined。传入 { spy: true } 可以在跟踪调用的同时继续调用真实实现,或者提供一个包含所需行为的工厂函数。
Class Mock 保留原型方法
从类 mock 创建的实例之前会继承 mock 自身的空 prototype。使用常规类语法定义的方法在实例上会是 undefined,即使是在构造函数内部也是如此,并且针对实现类的 instanceof 检查也会失败。这会影响 vi.fn(Dog)、带或不带 mock 实现的 vi.spyOn(obj, 'Dog'),以及 .mockImplementation(class ...)。
mock 的 prototype 现在会链接到实现类的 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 会连同实现一起恢复该原型链。有关详情,请参阅Mocking Classes。
基准测试 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 现在要求对 HTML 页面和 API 访问进行令牌认证。/__vitest__/ URL 在浏览器完成认证之前会显示错误。要进行认证,请使用 Vitest 打印的令牌打开该 URL,如下所示。认证完成后,直接访问 /__vitest__/ URL 将正常工作。
vitest --ui
# UI started at http://localhost:51204/__vitest__/?token=...Fake Timers and setSystemTime Now Mock Temporal
Vitest 现在会像 mock Date 一样 mock Temporal API,这遵循了 @sinonjs/fake-timers v15.4 的更新。只有当全局对象上存在 Temporal 时才会生效,无论它是原生提供的,还是通过全局安装的 polyfill(例如 import 'temporal-polyfill/global')提供的。
之前即使 vi.useFakeTimers() 处于激活状态,Temporal.Now 仍会返回真实的墙钟时间。现在它会跟随被模拟的时钟:
vi.useFakeTimers({ now: 0 })
Temporal.Now.instant().epochMilliseconds // 0(在 v4 中这里是实际时间)vi.setSystemTime() 也同样如此;此前在不使用 fake timers 时,它只会 mock Date:
vi.setSystemTime(0)
Temporal.Now.instant().epochMilliseconds // 0(在 v4 中是实际时间)Temporal 属于默认 fake API 集合,因此由 fakeTimers.toFake 和 fakeTimers.toNotFake 控制。要保留原生的 Temporal,请将其添加到 toNotFake:
vi.useFakeTimers({ toNotFake: ['Temporal'] })toThrow("") Matches Any Error Message
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> // 异步断言Vitest 不再从全局 jest.Matchers 接口读取自定义匹配器声明。同时支持 Jest 和 Vitest 的库应分别扩展 jest.Matchers 和 vitest.Matchers。这只会影响 TypeScript 声明;使用 expect.extend 注册匹配器的方式保持不变。
expect.poll 超时失败
expect.poll 现在会在其回调或轮询断言未能在 timeout 内完成时拒绝。此前,超出截止时间后才解析的回调,或者只有在较晚一次尝试中才通过的断言,仍然可能成功。现在回调还会接收一个 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.)失败。
未等待的异步断言会导致测试失败
异步断言(如 resolves、rejects 和 toMatchFileSnapshot)如果未使用 await,现在会导致测试失败。此前,Vitest 会在测试结束时自动等待它们,并打印警告:
test('unawaited assertion', async () => {
// v4:打印警告,测试通过
// v5:测试失败
expect(promise).resolves.toBe(1)
await expect(promise).resolves.toBe(1)
})报告的错误会指向未等待的断言。
测试标题和检查的值使用 pretty-format
Vitest 现在在检查值时使用 pretty-format 而不是 loupe 来格式化这些值,包括插入到 test.each 和 test.for 标题中的值。某些值的渲染会发生变化,因此捕获检查输出的快照或断言可能需要更新。
有两项变更专门针对生成的测试标题:
- 通过
$占位符插入的字符串值不再用引号包裹:
test.for([{ id: 'a1' }])('case $id', ({ id }) => { /* ... */ })
// v4 标题: case 'a1'
// v5 标题: 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。
// exact: true(默认)时,这里只会精确匹配字符串 "Hello, World"。
// exact: false 时,这里会匹配 "Hello, World!"、"Say Hello, World" 等。
const locator = page.getByText('Hello, World', { exact: true })
await locator.click()toHaveTextContent 现在执行严格相等匹配
浏览器模式下的 toHaveTextContent 匹配器现在会验证元素的文本内容是否与预期字符串完全相等,而不再执行部分、区分大小写的匹配。不再接受正则表达式。之前的行为,包括对 RegExp 的支持,已迁移到新的 toMatchTextContent 匹配器中。
// 部分匹配或正则匹配:
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)
// 精确匹配仍然使用 `toHaveTextContent`:
await expect.element(banner).toHaveTextContent('Error!')render 在 vitest-browser-vue 和 vitest-browser-svelte 中是异步的
配套的组件测试包 vitest-browser-vue 和 vitest-browser-svelte 现在会从 render 返回一个 promise,因此在查询渲染输出之前,必须先对该调用进行 await:
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'], // 匹配 src/**,而不是所有包含 "src" 的路径
},
},
})升级后请检查你的 include 和 exclude 模式,并确认报告出来的文件集合符合预期。以前仅因较宽松的匹配行为而被匹配到的文件,现在可能不会再被包含。
配置文件不会从父目录中查找
Vitest 不再向上搜索父目录中的配置文件。如果你之前依赖于在子目录中运行 vitest,同时使用父目录中的配置文件,请显式传入配置,并使用 --dir 限定测试发现范围。例如:
$ cd subdir && vitest
$ cd subdir && vitest --config ../vitest.config.tsDOM 环境中的全局赋值现在会更新底层窗口
在 jsdom 和 happy-dom 环境中,对 globalThis 或 window 上属性的赋值现在会传播到底层 DOM 实现。诸如 innerWidth 之类的可变属性会影响由 DOM 环境实现的 API,例如 happy-dom 的 matchMedia。
populateGlobal 在 originals 中返回描述符
populateGlobal 返回的 originals 映射现在保存的是属性描述符,而不是普通值。这样可以在捕获原始值时避免调用原生的延迟 getter(例如 Node 的 localStorage),并在清理时准确地恢复它们。
如果你在自定义环境中手动恢复它们,请使用 Object.defineProperty,而不是赋值:
originals.forEach((value, key) => (global[key] = value))
originals.forEach((descriptor, key) => Object.defineProperty(global, key, descriptor)) 浏览器协调器 URL 需要会话
Vitest 不再通过一个裸的 /__vitest_test__/ URL 提供浏览器协调器 UI。浏览器运行器 URL 现在绑定到会话,并且必须包含 Vitest 生成的 sessionId,例如 /__vitest_test__/?sessionId=...。
如果你之前是通过复制 Vite 服务器 URL 或直接访问 /__vitest_test__/ 来手动打开浏览器预览,请改用 Vitest 打开或打印的 URL。
browser.api Is Replaced by the Top-Level api
浏览器模式现在运行在由顶层 api 选项配置的单个 Vite 服务器上;浏览器模式下的默认端口仍然是 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 规则即可。本次主要版本迁移的默认位置变更如下:
- Attachments(
attachmentsDir):.vitest-attachements/→.vitest/attachments/ - Failure screenshots(
screenshotFailures):__screenshots__/→.vitest/attachments/failure-screenshots/,因此不再与toMatchScreenshot的参考截图混在一起 - Blob reporter 和
--merge-reports:.vitest-reports/blob-*.json→.vitest/blob/blob-*.json - HTML reporter(
html):html/index.html→.vitest/index.html,其选项也从outputFile(文件)改为outputDir(目录) - JSON reporter(
json):标准输出 →.vitest/json/output.json - JUnit reporter(
junit):标准输出 →.vitest/junit/output.xml
json 和 junit 报告器现在默认写入文件,而不是打印到标准输出。如果你之前通过管道传递报告(例如 vitest --reporter=json | jq),请改为读取工件文件,或者通过报告器的 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 和 pool 标识符现在从 1 开始,而不是 0。这会影响 VITEST_POOL_ID 和 VITEST_WORKER_ID 环境变量的值,它们现在的范围是从 1 到 worker 数量。请更新任何基于这些 id 推导值的逻辑,例如每个 worker 的数据库名称或数组索引。
对于自定义 reporter,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已被移除。
从 Jest 迁移
Vitest 的设计采用了与 Jest 兼容的 API,以使从 Jest 迁移尽可能简单。尽管做出了这些努力,您仍可能会遇到以下差异:
默认全局变量
Jest 默认启用了它们的全局 API。Vitest 没有。您可以通过 globals 配置设置 启用全局变量,或者更新代码以使用从 vitest 模块导入的内容。
如果您决定保持全局变量禁用,请注意像 testing-library 这样的常用库将不会运行自动 DOM 清理。
mock.mockReset
Jest 的 mockReset 会将 mock 实现替换为一个返回 undefined 的空函数。
Vitest 的 mockReset 会将 mock 实现重置为其原始状态。 也就是说,重置由 vi.fn(impl) 创建的 mock 会将 mock 实现重置为 impl。
mock.mock 是持久的
Jest 会在调用 .mockClear 时重新创建 mock 状态,这意味着您总是需要将其作为 getter 访问。另一方面,Vitest 持有状态的持久引用,这意味着您可以重用它:
const mock = vi.fn()
const state = mock.mock
mock.mockClear()
expect(state).toBe(mock.mock) // 在 Jest 中失败模块 Mock
在 Jest 中 Mock 模块时,工厂参数的返回值是默认导出。在 Vitest 中,工厂参数必须返回一个显式定义每个导出的对象。例如,以下 jest.mock 必须更新为:
jest.mock('./some-path', () => 'hello')
vi.mock('./some-path', () => ({
default: 'hello',
})) 更多详情请参阅 vi.mock API 部分。
自动 Mock 行为
与 Jest 不同,<root>/__mocks__ 中的 Mock 模块除非调用了 vi.mock(),否则不会加载。如果您需要像 Jest 一样在每个测试中 Mock 它们,可以在 setupFiles 中 Mock 它们。
导入被 Mock 包的原始版本
如果您只是部分 Mock 一个包,您之前可能使用了 Jest 的函数 requireActual。在 Vitest 中,您应该将这些调用替换为 vi.importActual。
const { cloneDeep } = jest.requireActual('lodash/cloneDeep')
const { cloneDeep } = await vi.importActual('lodash/cloneDeep') 将 Mock 扩展到外部库
Jest 默认会这样做:当 Mock 一个模块并希望此 Mock 扩展到其他使用相同模块的外部库时,您应该明确告诉要 Mock 哪个第三方库,以便外部库成为源代码的一部分,可通过使用 server.deps.inline。
server.deps.inline: ["lib-name"]expect.getState().currentTestName
Vitest 的 test 名称用 > 符号连接,以便更容易区分测试和套件,而 Jest 使用空格()。
- `${describeTitle} ${testTitle}`
+ `${describeTitle} > ${testTitle}`同样适用于 testNamePattern(-t 标志):Vitest 会匹配使用 > 连接的完整名称,而 Jest 会匹配使用空格连接的名称。请相应地更新跨越套件和测试的模式,或者匹配单个片段(-t adds),或在片段之间使用通配符(-t 'math.*adds')。
- vitest -t 'math adds'
+ vitest -t 'math > adds'环境变量
就像 Jest 一样,如果之前未设置,Vitest 会将 NODE_ENV 设置为 test。Vitest 还有一个对应于 JEST_WORKER_ID 的 VITEST_POOL_ID(总是小于或等于 maxWorkers),所以如果您依赖它,别忘了重命名它。Vitest 还暴露了 VITEST_WORKER_ID,这是运行 worker 的唯一 ID——这个数字不受 maxWorkers 影响,并且会随着每个创建的 worker 增加。
替换属性
如果您想修改对象,在 Jest 中会使用 replaceProperty API,您可以在 Vitest 中使用 vi.stubEnv 或 vi.spyOn 来做同样的事情。
Done 回调
Vitest 不支持以回调风格声明测试。您可以重写它们以使用 async/await 函数,或使用 Promise 来模拟回调风格。
it('should work', (done) => {
it('should work', () => new Promise(done => {
// ...
done()
})
})) 钩子
beforeAll/beforeEach 钩子在 Vitest 中可以返回清理函数。因此,如果它们返回 undefined 或 null 以外的内容,您可能需要重写钩子声明:
beforeEach(() => setActivePinia(createTestingPinia()))
beforeEach(() => { setActivePinia(createTestingPinia()) }) 在 Jest 中,钩子是顺序调用的(一个接一个)。默认情况下,Vitest 以栈方式运行钩子。要使用 Jest 的行为,请更新 sequence.hooks 选项:
export default defineConfig({
test: {
sequence: {
hooks: 'list',
}
}
})类型
Vitest 没有等同于 jest 命名空间的对象,所以您需要直接从 vitest 导入类型:
let fn: jest.Mock<(name: string) => number>
import type { Mock } from 'vitest'
let fn: Mock<(name: string) => number> 计时器
Vitest 不支持 Jest 的旧版计时器。
超时
如果您使用了 jest.setTimeout,您需要迁移到 vi.setConfig:
jest.setTimeout(5_000)
vi.setConfig({ testTimeout: 5_000 }) Vue 快照
这不是 Jest 特有的功能,但如果您之前使用带有 vue-cli 预设的 Jest,您将需要安装 jest-serializer-vue 包,并在 snapshotSerializers 中指定它:
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
snapshotSerializers: ['jest-serializer-vue']
}
})否则您的快照将会有很多转义的 " 字符。
自定义快照匹配器 experimental 4.1.3+
Jest 从 jest-snapshot 导入快照组合器。在 Vitest 中,请改用 vitest 中的 Snapshots:
const { toMatchSnapshot } = require('jest-snapshot')
import { Snapshots } from 'vitest'
const { toMatchSnapshot } = Snapshots
expect.extend({
toMatchTrimmedSnapshot(received: string, length: number) {
return toMatchSnapshot.call(this, received.slice(0, length))
},
})对于内联快照,同样适用:
const { toMatchInlineSnapshot } = require('jest-snapshot')
import { Snapshots } from 'vitest'
const { toMatchInlineSnapshot } = Snapshots
expect.extend({
toMatchTrimmedInlineSnapshot(received: string, inlineSnapshot?: string) {
return toMatchInlineSnapshot.call(this, received.slice(0, 10), inlineSnapshot)
},
})请参阅 自定义快照匹配器 获取完整指南。
从 Mocha + Chai + Sinon 迁移
Vitest 为从 Mocha+Chai+Sinon 测试套件迁移提供了极好的支持。虽然 Vitest 默认使用与 Jest 兼容的 API,但它也为 spy/mock 测试提供了 Chai 风格的断言,使迁移更容易。
测试结构
Mocha 和 Vitest 有相似的测试结构,但有一些差异:
// Mocha
describe('suite', () => {
before(() => { /* 设置 */ })
after(() => { /* 清理 */ })
beforeEach(() => { /* 设置 */ })
afterEach(() => { /* 清理 */ })
it('test', () => {
// 测试代码
})
})
// Vitest - 相同的结构也适用!
import { afterAll, afterEach, beforeAll, beforeEach, describe, it } from 'vitest'
describe('suite', () => {
beforeAll(() => { /* 设置 */ })
afterAll(() => { /* 清理 */ })
beforeEach(() => { /* 设置 */ })
afterEach(() => { /* 清理 */ })
it('test', () => {
// 测试代码
})
})断言
Vitest 默认包含 Chai 断言,所以 Chai 断言无需更改即可工作:
// Mocha+Chai 和 Vitest 均适用
import { expect } from 'vitest' // 或在 Mocha 中使用 'chai'
expect(value).to.equal(42)
expect(value).to.be.true
expect(array).to.have.lengthOf(3)
expect(obj).to.have.property('key')Spy/Mock 断言
Vitest 为 spies 和 mocks 提供了 Chai 风格断言,允许您从 Sinon 迁移而无需重写断言:
// 之前 (Mocha + Chai + Sinon)
const sinon = require('sinon')
const chai = require('chai')
const sinonChai = require('sinon-chai')
chai.use(sinonChai)
const spy = sinon.spy(obj, 'method')
obj.method('arg1', 'arg2')
expect(spy).to.have.been.called
expect(spy).to.have.been.calledOnce
expect(spy).to.have.been.calledWith('arg1', 'arg2')
// 之后 (Vitest) - 相同的断言语法!
import { expect, vi } from 'vitest'
const spy = vi.spyOn(obj, 'method')
obj.method('arg1', 'arg2')
expect(spy).to.have.been.called
expect(spy).to.have.been.calledOnce
expect(spy).to.have.been.calledWith('arg1', 'arg2')完整的 Chai 风格断言支持
Vitest 支持所有常见的 sinon-chai 断言:
| Sinon-Chai | Vitest | 描述 |
|---|---|---|
spy.called | called | Spy 至少被调用了一次 |
spy.calledOnce | calledOnce | Spy 恰好被调用了一次 |
spy.calledTwice | calledTwice | Spy 恰好被调用了两次 |
spy.calledThrice | calledThrice | Spy 恰好被调用了三次 |
spy.callCount(n) | callCount(n) | Spy 被调用了 n 次 |
spy.calledWith(...) | calledWith(...) | Spy 使用特定参数被调用 |
spy.calledOnceWith(...) | calledOnceWith(...) | Spy 使用特定参数被调用了一次 |
spy.returned(value) | returned | Spy 返回了特定值 |
请参阅 Chai 风格 Spy 断言 文档获取完整列表。
创建 Spies 和 Mocks
将 Sinon 的 spy/stub/mock 创建替换为 Vitest 的 vi 工具:
// Sinon
const sinon = require('sinon')
const spy = sinon.spy()
const stub = sinon.stub(obj, 'method')
const mock = sinon.mock(obj)
// Vitest
import { vi } from 'vitest'
const spy = vi.fn()
const stub = vi.spyOn(obj, 'method')
// Vitest 没有 "mocks" - 改用 spiesStub 返回值
// Sinon
stub.returns(42)
stub.onFirstCall().returns(1)
stub.onSecondCall().returns(2)
// Vitest
stub.mockReturnValue(42)
stub.mockReturnValueOnce(1)
stub.mockReturnValueOnce(2)Stub 实现
// Sinon
stub.callsFake(arg => arg * 2)
// Vitest
stub.mockImplementation(arg => arg * 2)恢复 Spies
// Sinon
spy.restore()
sinon.restore() // 恢复所有
// Vitest
spy.mockRestore()
vi.restoreAllMocks() // 恢复所有计时器
Sinon 和 Vitest 内部都使用 @sinonjs/fake-timers:
// Sinon
const clock = sinon.useFakeTimers()
clock.tick(1000)
clock.restore()
// Vitest
import { vi } from 'vitest'
vi.useFakeTimers()
vi.advanceTimersByTime(1000)
vi.useRealTimers()主要差异
- 全局变量:Mocha 默认提供全局变量。在 Vitest 中,要么从
vitest导入,要么启用globals配置 - 断言风格:您可以同时使用 Chai 风格 (
expect(spy).to.have.been.called) 和 Jest 风格 (expect(spy).toHaveBeenCalled()) - 并行执行:Vitest 默认并行运行测试,Mocha 顺序运行
更多信息,请参阅:
