Skip to content

迁移指南

迁移到 Vitest 4.0

V8 Code Coverage Major Changes

Vitest 的 V8 覆盖率提供器现在使用了更精准的结果映射逻辑,从 Vitest v3 升级后,你可能会看到覆盖率报告的内容有变化。

之前 Vitest 使用 v8-to-istanbul 将 V8 覆盖率结果映射到源码文件,但这种方式不够准确,报告中常常会出现误报。现在我们开发了基于 AST 分析的新方法,使 V8 报告的准确度与 @vitest/coverage-istanbul 一致。

  • 覆盖率忽略提示已更新,详见 覆盖率 | 忽略代码
  • 已移除 coverage.ignoreEmptyLines 选项。没有可执行代码的行将不再出现在报告中。
  • 已移除 coverage.experimentalAstAwareRemapping 选项。此功能现已默认启用,并成为唯一的映射方式。
  • 现在 V8 提供器也支持 coverage.ignoreClassMethods

移除 coverage.allcoverage.extensions 选项

在之前的版本中,Vitest 会默认把所有未覆盖的文件包含到报告中。这是因为 coverage.all 默认为 truecoverage.include 默认为 **。这样设计是因为测试工具无法准确判断用户源码所在位置。

然而,这导致 Vitest 覆盖率工具会处理很多意料之外的文件(例如压缩 JS 文件),造成报告生成速度很慢甚至卡死。在 Vitest v4 中,我们彻底移除了 coverage.all,并将默认行为改为只在报告中包含被测试覆盖的文件

在升级到 v4 后,推荐在配置中显式指定 coverage.include,并视需要配合使用 coverage.exclude 进行排除。

vitest.config.ts
ts
export default defineConfig({
  test: {
    coverage: {
      // 包含匹配此模式的被覆盖和未覆盖文件:
      include: ['packages/**/src/**.{js,jsx,ts,tsx}'], 

      // 对上述 include 匹配到的文件应用排除规则:
      exclude: ['**/some-pattern/**'], 

      // 以下选项已移除
      all: true, 
      extensions: ['js', 'ts'], 
    }
  }
})

如果未定义 coverage.include,报告将只包含测试运行中被加载的文件:

vitest.config.ts
ts
export default defineConfig({
  test: {
    coverage: {
      // 未设置 include,只包含运行时加载的文件
      include: undefined, 

      // 匹配此模式的已加载文件将被排除:
      exclude: ['**/some-pattern/**'], 
    }
  }
})

更多示例请参考:

spyOn and fn 支持构造函数

在之前版本中,如果你对构造函数使用 vi.spyOn,会收到类似 Constructor <name> requires 'new' 的错误。从 Vitest 4 开始,所有用 new 调用的 mock 都会正确创建实例,而不是调用 mock.apply。这意味着 mock 实现必须使用 functionclass 关键字,例如:

ts
const cart = {
  Apples: class Apples {
    getApples() {
      return 42
    }
  }
}

const Spy = vi.spyOn(cart, 'Apples')
  .mockImplementation(() => ({ getApples: () => 0 })) 
  // 使用 function 关键字
  .mockImplementation(function () {
    this.getApples = () => 0
  })
  // 使用自定义 class
  .mockImplementation(class MockApples {
    getApples() {
      return 0
    }
  })

const mock = new Spy()

请注意,如果此时使用箭头函数,调用 mock 时会报 <anonymous> is not a constructor 错误

Mock 的变更

Vitest 4 除新增构造函数支持外,还重构了 mock 的创建机制,一举修复多年累积的模块模拟顽疾;尤其在类与 spy 交互时,行为更易预测、不再烧脑。

  • vi.fn().getMockName() 现默认返回 vi.fn(),而不再附带 spy。这一改动会使快照中的 mock 名称从 [MockFunction spy] 简化为 [MockFunction];而 vi.spyOn 创建的 spy 仍沿用原始名称,便于调试。
  • vi.restoreAllMocks 现已缩小作用范围:仅还原由 vi.spyOn 手动创建的 spy ,不再触及自动 mock ,亦不会重置其内部状态(对应配置项 restoreMocks 同步更新)。.mockRestore 仍按原行为重置实现并清空状态。
  • 现对 mock 调用 vi.spyOn 时,返回的仍是原 mock,而非新建 spy。
  • 自动 mock 的实例方法已正确隔离,但仍与原型共享底层状态;除非方法已自定义 mock 实现,否则修改原型实现会同步影响所有实例。此外,调用 .mockReset 不再破坏此继承关系。
ts
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)
  • 自动 mock 方法一经生成即不可还原,手动 .mockRestore 无效;spy: true 的自动 mock 模块行为保持不变。
  • 自动 mock 的 getter 不再执行原始逻辑,默认返回 undefined;如需继续监听并改写,请使用 vi.spyOn(object, name, 'get')
  • 执行 vi.fn(implementation).mockReset() 后,.getMockImplementation() 现可正确返回原 mock 实现。
  • vi.fn().mock.invocationCallOrder 现以 1 起始,与 Jest 保持一致。

带文件名过滤器的独立模式

为了提升用户体验,当 --standalone 与文件名过滤器一起使用时,Vitest 现在会直接开始运行匹配到的文件。

sh
# 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.ts

这允许用户为独立模式创建可复用的 package.json

json
{
  "scripts": {
    "test:dev": "vitest --standalone"
  }
}
bash
# 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.ts

Replacing vite-node with Module Runner

Module Runner 已取代 vite-node,直接内嵌于 Vite, Vitest 亦移除 SSR 封装,直接调用。主要变更如下:

  • 环境变量:VITE_NODE_DEPS_MODULE_DIRECTORIESVITEST_MODULE_DIRECTORIES
  • 注入字段:__vitest_executormoduleRunnerModuleRunner 实例)
  • 移除内部入口 vitest/execute
  • 自定义环境用 viteEnvironment 取代 transformMode;未指定时,Vitest 以环境名匹配 server.environments
  • 依赖列表剔除 vite-node
  • deps.optimizer.web 重命名为 deps.optimizer.client,并支持自定义环境名

Vite 已提供外部化机制,但为降低破坏性,仍保留旧方案;server.deps 可继续用于包的内联/外部化。

未使用上述高级功能者,升级无感知。

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:

ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    workspace: './vitest.workspace.js', 
    projects: [ 
      './packages/*', 
      { 
        test: { 
          name: 'unit', 
        }, 
      }, 
    ] 
  }
})
ts
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.

ts
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:

ts
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:

ts
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.

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:

ts
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:

ts
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.

js
// 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 移除了以下废弃的配置项:

  • poolMatchGlobs 配置项。请使用 projects 代替。
  • environmentMatchGlobs 配置项。请使用 projects 代替。
  • deps.externaldeps.inlinedeps.fallbackCJS 配置项。请改用 server.deps.externalserver.deps.inlineserver.deps.fallbackCJS
  • browser.testerScripts 配置项。请使用 browser.testerHtmlPath 代替。
  • minWorkers 配置项。只有 maxWorkers 会对测试运行方式产生影响,因此我们正在移除这个公共选项。
  • Vitest 不再支持将测试选项作为第三个参数提供给 testdescribe。请改用第二个参数。
ts
test('example', () => { /* ... */ }, { retry: 2 }) 
test('example', { retry: 2 }, () => { /* ... */ }) 

同时,所有弃用类型被一次性清理,彻底解决误引 @types/node 的问题(#5481#6141)。

从 Jest 迁移

Vitest 的 API 设计兼容 Jest,旨在使从 Jest 迁移尽可能简单。尽管如此,你仍可能遇到以下差异:

默认是否启用全局变量

Jest 默认启用其 globals API。Vitest 默认不启用。你可以通过配置项 globals 启用全局变量,或者修改代码直接从 vitest 模块导入所需 API。

如果选择不启用全局变量,注意常用库如 testing-library 将不会自动执行 DOM 的 清理

mock.mockReset

Jest 的 mockReset 会将 mock 实现替换为空函数,返回 undefined

Vitest 的 mockReset 会将 mock 实现重置为最初的实现。也就是说,使用 vi.fn(impl) 创建的 mock,mockReset 会将实现重置为 impl

mock.mock 是持久的

Jest 调用 .mockClear 后会重建 mock 状态,只能以 getter 方式访问; Vitest 则保留持久引用,可直接复用。

ts
const mock = vi.fn()
const state = mock.mock
mock.mockClear()

expect(state).toBe(mock.mock) // fails in Jest

模块 Mock

在 Jest 中,mock 模块时工厂函数返回值即为默认导出。在 Vitest 中,工厂函数需返回包含所有导出的对象。例如,以下 Jest 代码需要改写为:

ts
jest.mock('./some-path', () => 'hello') 
vi.mock('./some-path', () => ({ 
  default: 'hello', 
})) 

更多细节请参考 vi.mock API

自动 Mock 行为

与 Jest 不同,Vitest 仅在调用 vi.mock() 时加载 <root>/__mocks__ 中的模块。如果你需要像 Jest 一样在每个测试中自动 mock,可以在 setupFiles 中调用 mock。

导入被 Mock 包的原始模块

如果只部分 mock 一个包,之前可能用 Jest 的 requireActual,Vitest 中应使用 vi.importActual

ts
const { cloneDeep } = jest.requireActual('lodash/cloneDeep') 
const { cloneDeep } = await vi.importActual('lodash/cloneDeep') 

扩展 Mock 到外部库

Jest 默认会扩展 mock 到使用相同模块的外部库。Vitest 需要显式告知要 mock 的第三方库,使其成为源码的一部分,方法是使用 server.deps.inline

server.deps.inline: ["lib-name"]

expect.getState().currentTestName

Vitest 的测试名使用 > 符号连接,方便区分测试与套件,而 Jest 使用空格 ()。

diff
- `${describeTitle} ${testTitle}`
+ `${describeTitle} > ${testTitle}`

环境变量

与 Jest 类似,Vitest 会将未设置时的 NODE_ENV 设为 test。Vitest 还有对应 JEST_WORKER_IDVITEST_POOL_ID(小于等于 maxThreads),如果依赖此值,需重命名。Vitest 还暴露 VITEST_WORKER_ID,表示唯一的运行中 worker ID,受 maxThreads 不影响,随 worker 创建递增。

替换属性

如果想修改对象,Jest 使用 replaceProperty API,Vitest 可使用 vi.stubEnvvi.spyOn 达成相同效果。

Done 回调

Vitest 不支持回调式测试声明。你可以改写为使用 async/await 函数,或使用 Promise 来模拟回调风格。

js
it('should work', (done) => {  
it('should work', () => new Promise(done => { 
  // ...
  done()
}) 
})) 

Hooks

Vitest 中 beforeAll/beforeEach 钩子可返回 清理函数。因此,如果钩子返回非 undefinednull,可能需改写:

ts
beforeEach(() => setActivePinia(createTestingPinia())) 
beforeEach(() => { setActivePinia(createTestingPinia()) }) 

在 Jest 中钩子是顺序执行的(一个接一个)。默认情况下,Vitest 在栈中运行钩子。要使用 Jest 的行为,请更新 sequence.hooks 选项:

ts
export default defineConfig({
  test: {
    sequence: { 
      hooks: 'list', 
    } 
  }
})

类型

Vitest 没有 Jest 的 jest 命名空间,需直接从 vitest 导入类型:

ts
import type { Mock } from 'vitest' let fn: jest.Mock<(name: string) => number> 
let fn: Mock<(name: string) => number> 

定时器

Vitest 不支持 Jest 的遗留定时器。

超时

如果使用了 jest.setTimeout,需迁移为 vi.setConfig

ts
jest.setTimeout(5_000) 
vi.setConfig({ testTimeout: 5_000 }) 

Vue 快照

这不是 Jest 特有的功能,但如果你之前在 vue-cli 预设中使用 Jest,你需要安装 jest-serializer-vue 包,并在 snapshotSerializers 中指定它:

vitest.config.js
js
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    snapshotSerializers: ['jest-serializer-vue']
  }
})

否则快照中会出现大量转义的 " 字符。

Released under the MIT License.