Skip to content

测试运行生命周期

TIP

正在寻找 beforeEachafterEach 和其他钩子的实用入门介绍?请参阅 设置与清理 教程。

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

概述

典型的 Vitest 测试运行会经历以下主要阶段:

  1. 初始化:配置加载和项目设置
  2. 全局设置:在任何测试运行之前的一次性设置
  3. Worker 创建:根据 pool 配置生成测试 worker
  4. 测试文件收集:发现并组织测试文件
  5. 测试执行:测试与其钩子和断言一起运行
  6. 报告:收集并报告结果
  7. 全局清理:所有测试完成后的最终清理

阶段 4–6 为每个测试文件运行一次,因此在整个测试套件中它们将执行多次,并且当你使用多于 1 个 worker 时,它们也可能在不同文件之间并行运行。

详细生命周期阶段

1. 初始化阶段

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

发生什么:

如果配置文件或其导入之一发生更改,此阶段可以再次运行。

范围: 主进程(在任何测试 worker 创建之前)

2. 全局设置阶段

如果你配置了 globalSetup 文件,它们会在任何测试 worker 创建之前运行一次。

发生什么:

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

范围: 主进程(与测试 worker 分离)

重要说明:

  • 全局设置在与测试不同的全局范围中运行
  • 测试无法访问全局设置中定义的变量(使用 provide/inject 代替)
  • 只有当至少有一个测试排队时,全局设置才会运行
globalSetup.ts
ts
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.enabledpool 设置生成工作线程(threadsforksvmThreadsvmForks
  • 每个工作线程都有自己的隔离环境(除非禁用 隔离
  • 默认情况下,为了提供隔离,工作线程不会被复用。只有在以下情况下才会复用工作线程:
    • 禁用 隔离
    • 或者池是 vmThreadsvmForks,因为 VM 提供了足够的隔离

范围: Worker 进程/线程

4. 测试文件设置阶段

在每个测试文件运行之前,执行 设置文件

发生什么:

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

范围: Worker 进程(与测试相同)

重要说明:

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

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

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

5. 测试收集和执行阶段

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

测试文件执行顺序

测试文件根据你的配置执行:

  • 在 worker 内默认顺序执行
  • 文件将在不同 worker 之间并行运行,由 maxWorkers 配置
  • 顺序可以通过 sequence.shuffle 随机化,或通过 sequence.sequencer 微调
  • 长时间运行的测试通常更早开始(基于缓存),除非启用了 shuffle

在每个测试文件内

执行遵循以下顺序:

  1. 文件级代码: describe 块之外的所有代码都会立即运行
  2. 测试收集: describe 块会被处理,测试会作为导入测试文件的副作用而注册
  3. aroundAll 钩子: 包裹套件中的所有测试(必须调用 runSuite()
  4. beforeAll 钩子: 在套件中的任何测试之前运行一次
  5. 对于每个测试:
    • aroundEach 钩子包裹测试(必须调用 runTest()
    • beforeEach 钩子执行(按定义顺序,或基于 sequence.hooks
    • 测试函数执行
    • afterEach 钩子执行(默认情况下在 sequence.hooks: 'stack' 时按逆序)
    • beforeEach 钩子返回的清理函数执行(默认情况下在 sequence.hooks: 'stack' 时按逆序)
    • onTestFinished 回调运行(始终按逆序)
    • 如果测试失败:onTestFailed 回调运行
    • 注意:如果设置了 repeatsretry,则所有这些步骤都会再次执行
  6. afterAll 钩子: 在套件中的所有测试完成后运行一次
  7. 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')
  })
})

// 输出:
// 文件已加载
// 套件已定义
// aroundAll 之前
//   beforeAll
//   aroundEach 之前
//     beforeEach
//       测试 1
//     afterEach
//     beforeEachCleanup
//   aroundEach 之后
//   aroundEach 之前
//     beforeEach
//       测试 2
//     afterEach
//     beforeEachCleanup
//   aroundEach 之后
//   afterAll
//   beforeAllCleanup
// aroundAll 之后

嵌套套件

使用嵌套 describe 块时,钩子遵循层次模式。aroundAllaroundEach 钩子包裹它们各自的范围,父钩子包裹子钩子:

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'))
})

// 输出:
// 外层 aroundAll 之前
//   outer beforeAll
//   outer aroundEach 之前
//     outer beforeEach
//       outer test
//     outer afterEach
//   outer aroundEach 之后
//   inner aroundAll 之前
//     inner beforeAll
//     outer aroundEach 之前
//       inner aroundEach 之前
//         outer beforeEach
//           inner beforeEach
//             inner test
//           inner afterEach
//         outer afterEach
//       inner aroundEach 之后
//     outer aroundEach 之后
//     inner afterAll
//   inner aroundAll 之后
//   outer afterAll
// outer aroundAll 之后

并发测试

当使用 test.concurrentsequence.concurrent 时:

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

6. 报告阶段

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

发生什么:

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

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

7. 全局清理阶段

所有测试完成后,全局清理函数执行。

发生什么:

  • globalSetup 文件中的 teardown() 函数运行
  • 多个清理函数按其设置的逆序运行
  • 在 watch 模式下,清理在进程退出前运行,而不是在测试重新运行之间

范围: 主进程

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

不同作用域中的生命周期

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

阶段作用域访问测试上下文运行次数
配置文件主进程❌ 否每次 Vitest 运行一次
全局设置主进程❌ 否(使用 provide/inject每次 Vitest 运行一次
设置文件工作线程(与测试相同)✅ 是每个测试文件之前
文件级代码工作线程✅ 是每个测试文件一次
aroundAll工作线程✅ 是每个套件一次(包裹所有测试)
beforeAll / afterAll工作线程✅ 是每个套件一次
aroundEach工作线程✅ 是每个测试(包裹每个测试)
beforeEach / afterEach工作线程✅ 是每个测试
测试函数工作线程✅ 是一次(重试/重复时可能多次)
全局清理主进程❌ 否每次 Vitest 运行一次

监视模式生命周期

在监视模式下,生命周期会重复,但有一些区别:

  1. 初始运行:完整的生命周期,如上所述
  2. 文件变更时
  3. 退出时
    • 执行全局清理
    • 进程终止

性能考量

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

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

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

相关文档