第4章 Vitest 测试运行生命周期详解
测试运行生命周期
理解测试运行生命周期对于编写高效测试、调试问题以及优化测试套件至关重要。本指南介绍了 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