进阶 vitest.dev 2026-10-22 19:00:41 · 18 阅读

第4章 Vitest 测试运行生命周期详解

测试运行生命周期

寻找 beforeEach、afterEach 及其他钩子的实用入门教程?请参见“Setup 与 Teardown 教程”。

理解测试运行生命周期对于编写高效测试、调试问题以及优化测试套件至关重要。本指南介绍了 Vitest 中不同生命周期阶段的触发时机与执行顺序,从初始化到销毁。

概览

典型的 Vitest 测试运行包含以下主要阶段:

  • 初始化(Initialization):加载配置并设置项目
  • 全局设置(Global Setup):在任何测试运行前的一次性设置
  • Worker 创建(Worker Creation):根据池配置生成测试 Worker
  • 测试文件收集(Test File Collection):发现并组织测试文件
  • 测试执行(Test Execution):运行测试及其钩子和断言
  • 报告(Reporting):收集和报告结果
  • 全局销毁(Global Teardown):所有测试完成后的最终清理

阶段 4–6 对每个测试文件都会执行一次,因此在整个测试套件中会多次运行;当使用多于 1 个 Worker 时,不同文件间的执行还可能并行。

详细生命周期阶段

1. 初始化阶段

运行 vitest 时,框架首先加载配置并准备测试环境。

发生的事项:

  • 解析命令行参数
  • 加载配置文件
  • 验证项目结构

如果配置文件或其导入项发生变化,此阶段可能再次运行。

作用域:主进程(在创建任何测试 Worker 之前)

2. 全局设置阶段

若配置了 globalSetup 文件,它们会在任何测试 Worker 创建前运行一次。

发生的事项:

  • 全局设置文件中的 setup() 函数(或导出的默认函数)按顺序执行
  • 多个全局设置文件按定义顺序运行

作用域:主进程(独立于测试 Worker)

重要说明:

  • 全局设置在与测试不同的全局作用域中运行
  • 测试无法访问全局设置中定义的变量(请改用 provide/inject)
  • 只有当至少有一个测试排队时,全局设置才会运行
globalSetup.tststsx
export function setup(project) {
  // 在所有测试前运行一次
  console.log('Global setup')

  // 与测试共享数据
  project.provide('apiUrl', 'http://localhost:3000')
}

export function teardown() {
  // 在所有测试后运行一次
  console.log('Global teardown')
}

3. Worker 创建阶段

全局设置完成后,Vitest 根据你的池配置创建测试 Worker。

发生的事项:

  • 根据 browser.enabled 或 pool 设置(threads, forks, vmThreads, 或 vmForks)生成 Worker
  • 每个 Worker 拥有独立的隔离环境(除非禁用隔离)
  • 默认情况下,为了提供隔离,Worker 不会被复用。仅在以下情况下复用 Worker:隔离被禁用,或者池为 vmThreads/vmForks(因为 VM 提供了足够的隔离)

作用域:Worker 进程/线程

4. 测试文件设置阶段

在每个测试文件运行前,设置文件会被执行。

发生的事项:

  • 设置文件在与你测试相同的进程中运行
  • 默认情况下,设置文件并行运行(可通过 sequence.setupFiles 配置)
  • 设置文件在每个测试文件之前执行
  • 可在此处初始化任何全局状态或配置

作用域:Worker 进程(与你的测试相同)

重要说明:

  • 若禁用隔离,设置文件仍会在每个测试文件前重新运行以触发副作用,但导入的模块会被缓存
  • 在 watch 模式下,编辑设置文件会触发该文件所在测试套件中所有测试的重跑
setupFile.tstst
import { afterEach } from 'vitest'

// 在每个测试文件前运行
console.log('Setup file executing')

// 注册适用于所有测试的钩子
afterEach(() => {
  cleanup()
})

5. 测试收集与执行阶段

这是测试实际运行的主要阶段。

测试文件执行顺序

测试文件的执行基于你的配置:

  • 在单个 Worker 内默认顺序执行
  • 不同 Worker 间的文件会并行运行,由 maxWorkers 配置
  • 可通过 sequence.shuffle 随机化顺序,或用 sequence.sequencer 微调
  • 基于缓存,耗时较长的测试通常会更早启动(除非启用 shuffle)

每个测试文件内部

执行遵循以下顺序:

  • 文件级代码:describe 块外的所有代码立即执行
  • 测试收集:处理 describe 块,将测试注册为导入测试文件的副作用
  • aroundAll 钩子:包裹套件中的所有测试(必须调用 runSuite())
  • beforeAll 钩子:在套件中任何测试运行前执行一次
  • 对于每个测试:
    • aroundEach 钩子包裹单个测试(必须调用 runTest())
    • beforeEach 钩子执行(按定义顺序,或基于 sequence.hooks)
    • 测试函数执行
    • afterEach 钩子执行(默认情况下 sequence.hooks 为 'stack' 时按逆序执行)
    • 从 beforeEach 钩子返回的清理函数执行(默认情况下 sequence.hooks 为 'stack' 时按逆序执行)
    • onTestFinished 回调运行(始终按逆序)
    • 如果测试失败:onTestFailed 回调运行
  • 注意:如果设置了 repeats 或 retry,上述所有步骤会再次执行
  • afterAll 钩子:在套件中所有测试完成后执行一次
  • 从 beforeAll 钩子返回的清理函数:在套件中所有测试完成后执行一次

示例执行流程:

ts
// 这立即运行(收集阶段)
console.log('File loaded')

describe('User API', () => {
  // 这立即运行(收集阶段)
  console.log('Suite defined')

  aroundAll(async (runSuite) => {
    // 包裹此套件中的所有测试
    console.log('aroundAll before')
    await runSuite()
    console.log('aroundAll after')
  })

  beforeAll(() => {
    // 在此套件的所有测试前运行一次
    console.log('beforeAll')

    return function beforeAllCleanup() {
      // 在 afterAll 钩子运行后执行一次
      console.log('beforeAllCleanup')
    }
  })

  aroundEach(async (runTest) => {
    // 包裹每个测试
    console.log('aroundEach before')
    await runTest()
    console.log('aroundEach after')
  })

  beforeEach(() => {
    // 在每个测试前运行
    console.log('beforeEach')

    return function beforeEachCleanup() {
      // 在 afterEach 钩子运行后执行
      console.log('beforeEachCleanup')
    }
  })

  test('creates user', () => {
    // 测试执行
    console.log('test 1')
  })

  test('updates user', () => {
    // 测试执行
    console.log('test 2')
  })

  afterEach(() => {
    // 在每个测试后运行
    console.log('afterEach')
  })

  afterAll(() => {
    // 在此套件的所有测试后运行一次
    console.log('afterAll')
  })
})

// 输出:
// File loaded
// Suite defined
// aroundAll before
//   beforeAll
//   aroundEach before
//     beforeEach
//       test 1
//     afterEach
//     beforeEachCleanup
//   aroundEach after
//   aroundEach before
//     beforeEach
//       test 2
//     afterEach
//     beforeEachCleanup
//   aroundEach after
//   afterAll
//   beforeAllCleanup
// aroundAll after

嵌套套件

使用嵌套的 describe 块时,钩子遵循层级模式。aroundAll 和 aroundEach 钩子包裹其各自的作用域,父级钩子包裹子级钩子:

ts
describe('outer', () => {
  aroundAll(async (runSuite) => {
    console.log('outer aroundAll before')
    await runSuite()
    console.log('outer aroundAll after')
  })

  beforeAll(() => console.log('outer beforeAll'))

  aroundEach(async (runTest) => {
    console.log('outer aroundEach before')
    await runTest()
    console.log('outer aroundEach after')
  })

  beforeEach(() => console.log('outer beforeEach'))

  test('outer test', () => console.log('outer test'))

  describe('inner', () => {
    aroundAll(async (runSuite) => {
      console.log('inner aroundAll before')
      await runSuite()
      console.log('inner aroundAll after')
    })

    beforeAll(() => console.log('inner beforeAll'))

    aroundEach(async (runTest) => {
      console.log('inner aroundEach before')
      await runTest()
      console.log('inner aroundEach after')
    })

    beforeEach(() => console.log('inner beforeEach'))

    test('inner test', () => console.log('inner test'))

    afterEach(() => console.log('inner afterEach'))
    afterAll(() => console.log('inner afterAll'))
  })

  afterEach(() => console.log('outer afterEach'))
  afterAll(() => console.log('outer afterAll'))
})

// 输出:
// outer aroundAll before
//   outer beforeAll
//   outer aroundEach before
//     outer beforeEach
//       outer test
//     outer afterEach
//   outer aroundEach after
//   inner aroundAll before
//     inner beforeAll
//     outer aroundEach before
//       inner aroundEach before
//         outer beforeEach
//           inner beforeEach
//             inner test
//           inner afterEach
//         outer afterEach
//       inner aroundEach after
//     outer aroundEach after
//     inner afterAll
//   inner aroundAll after
//   outer afterAll
// outer aroundAll after

并发测试

使用 test.concurrent 或 sequence.concurrent 时:

  • 同一文件内的测试可并行运行
  • 每个并发测试仍运行自己的 beforeEach 和 afterEach 钩子
  • 并发快照使用测试上下文:test.concurrent('name', async ({ expect }) => {})

6. 报告阶段

在整个测试运行期间,报告器接收生命周期事件并显示结果。

发生的事项:

  • 随着测试进展,报告器接收事件
  • 结果被收集和格式化
  • 生成测试摘要
  • 生成覆盖率报告(如果启用)

有关报告器生命周期的详细信息,请参阅报告器指南。

7. 全局销毁阶段

所有测试完成后,全局销毁函数执行。

发生的事项:

  • globalSetup 文件中的 teardown() 函数运行
  • 多个销毁函数按设置的逆序运行
  • 在 watch 模式下,销毁在进程退出前运行,而非在测试重跑之间

作用域:主进程

globalSetup.tstsx
export function teardown() {
  // 清理全局资源
  console.log('Global teardown complete')
}

不同作用域中的生命周期

了解代码执行的位置对于避免常见陷阱至关重要:

阶段 作用域 访问测试上下文 运行频率
配置文件 主进程 ❌ 否 每次 Vitest 运行一次
全局设置 主进程 ❌ 否(使用 provide/inject) 每次 Vitest 运行一次
设置文件 Worker(与测试相同) ✅ 是 每个测试文件前
文件级代码 Worker ✅ 是 每个测试文件一次
aroundAll Worker ✅ 是 每个套件一次(包裹所有测试)
beforeAll / afterAll Worker ✅ 是 每个套件一次
aroundEach Worker ✅ 是 每个测试(包裹单个测试)
beforeEach / afterEach Worker ✅ 是 每个测试
测试函数 Worker ✅ 是 一次(若有重试/重复则更多次)
全局销毁 主进程 ❌ 否 每次 Vitest 运行一次

Watch 模式生命周期

在 watch 模式下,生命周期会重复,但有一些不同:

  • 初始运行:如上述描述的完整生命周期
  • 文件更改时:
    • 开始新的测试运行
    • 仅重跑受影响的测试文件
    • 针对这些测试文件再次运行设置文件
    • 全局设置不会重跑(特定于重跑的逻辑请使用 project.onTestsRerun)
  • 退出时:
    • 执行全局销毁
    • 终止进程

性能考量

理解生命周期有助于优化测试性能:

  • 全局设置非常适合昂贵的一次性操作(数据库播种、服务器启动)
  • 设置文件在每个测试文件前运行——如果你有多个测试文件,请避免在此处执行繁重操作
  • 对于不需要隔离的昂贵设置,beforeAll 优于 beforeEach
  • 禁用隔离可提高性能,但设置文件仍会在每个文件前执行
  • 池配置影响并行化和可用 API

有关提高性能的技巧,请阅读提高性能指南。

相关文档

  • 全局设置配置
  • 设置文件配置
  • 测试排序选项
  • 隔离配置
  • 池配置
  • 扩展报告器 - 关于报告器生命周期事件
  • 测试 API 参考 - 关于钩子 API

评论 (0)