--- url: /guide/lifecycle.md --- # 测试运行生命周期 ::: tip 正在寻找 `beforeEach`、`afterEach` 和其他钩子的实用入门介绍?请参阅 [设置与清理](/guide/learn/setup-teardown) 教程。 ::: 理解测试运行生命周期对于编写有效的测试、调试问题以及优化测试套件至关重要。本指南介绍了 Vitest 中不同生命周期阶段何时以及以何种顺序发生,从初始化到清理。 ## 概述 典型的 Vitest 测试运行会经历以下主要阶段: 1. **初始化**:配置加载和项目设置 2. **全局设置**:在任何测试运行之前的一次性设置 3. **Worker 创建**:根据 [pool](/config/pool) 配置生成测试 worker 4. **测试文件收集**:发现并组织测试文件 5. **测试执行**:测试与其钩子和断言一起运行 6. **报告**:收集并报告结果 7. **全局清理**:所有测试完成后的最终清理 阶段 4–6 为每个测试文件运行一次,因此在整个测试套件中它们将执行多次,并且当你使用多于 [1 个 worker](/config/maxworkers) 时,它们也可能在不同文件之间并行运行。 ## 详细生命周期阶段 ### 1. 初始化阶段 当你运行 `vitest` 时,框架首先加载你的配置并准备测试环境。 **发生什么:** * 解析[命令行](/guide/cli)参数 * 加载[配置文件](/config/) * 验证项目结构 如果配置文件或其导入之一发生更改,此阶段可以再次运行。 **范围:** 主进程(在任何测试 worker 创建之前) ### 2. 全局设置阶段 如果你配置了 [`globalSetup`](/config/globalsetup) 文件,它们会在任何测试 worker 创建之前运行一次。 **发生什么:** * 全局设置文件中的 `setup()` 函数(或导出的 `default` 函数)按顺序执行 * 多个全局设置文件按定义顺序运行 **范围:** 主进程(与测试 worker 分离) **重要说明:** * 全局设置在与测试**不同的全局范围**中运行 * 测试无法访问全局设置中定义的变量(请改用 [`provide`/`inject`](/config/provide)) * 只有至少有一个测试排队时,全局设置才会运行 ```ts [globalSetup.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 根据你的 [池配置](/config/pool) 创建测试 worker。 **发生什么:** * 根据 `browser.enabled` 或 `pool` 设置启动 worker(`threads`、`forks`、`vmThreads` 或 `vmForks`) * 每个 worker 都有独立的环境(除非禁用了[隔离](/config/isolate)) * 默认情况下,为了提供隔离,worker 不会复用。只有在以下情况下才会复用: * 禁用了[隔离](/config/isolate) * 或者池为 `vmThreads` 或 `vmForks`,因为 [VM](https://nodejs.org/api/vm.html) 提供了足够的隔离 **范围:** Worker 进程/线程 ### 4. 测试文件设置阶段 在每个测试文件运行之前,执行 [设置文件](/config/setupfiles)。 **发生什么:** * 设置文件与测试在同一进程中运行 * 默认情况下,设置文件会**并行**运行(可通过 [`sequence.setupFiles`](/config/sequence#sequence-setupfiles) 配置) * 设置文件在**每个测试文件**之前执行 * 可以在这里初始化全局\_状态\_或配置 **范围:** Worker 进程(与测试相同) **重要说明:** * 即使禁用了[隔离](/config/isolate),设置文件仍会在每个测试文件之前重新运行以触发副作用,但导入的模块会被缓存 * 在 watch 模式下,编辑设置文件会触发所有测试重新运行 ```ts [setupFile.ts] import { afterEach } from 'vitest' // 在每个测试文件之前运行 console.log('Setup file executing') // 注册适用于所有测试的钩子 afterEach(() => { cleanup() }) ``` ### 5. 测试收集和执行阶段 这是测试实际运行的主要阶段。 #### 测试文件执行顺序 测试文件根据你的配置执行: * 在 worker 内默认**顺序**执行 * 文件将在不同 worker 之间**并行**运行,由 [`maxWorkers`](/config/maxworkers) 配置 * 顺序可以通过 [`sequence.shuffle`](/config/sequence#sequence-shuffle) 随机化,或通过 [`sequence.sequencer`](/config/sequence#sequence-sequencer) 微调 * 长时间运行的测试通常更早开始(基于缓存),除非启用了 shuffle #### 在每个测试文件内 执行遵循以下顺序: 1. **文件级代码:** `describe` 块之外的所有代码都会立即运行 2. **测试收集:** `describe` 块会被处理,测试会作为导入测试文件的副作用而注册 3. **[`aroundAll`](/api/hooks#aroundall) 钩子:** 包裹套件中的所有测试(必须调用 `runSuite()`) 4. **[`beforeAll`](/api/hooks#beforeall) 钩子:** 在套件中的任何测试之前运行一次 5. **对于每个测试:** * [`aroundEach`](/api/hooks#aroundeach) 钩子包裹测试(必须调用 `runTest()`) * `beforeEach` 钩子执行(按定义顺序,或基于 [`sequence.hooks`](/config/sequence#sequence-hooks)) * 测试函数执行 * `afterEach` 钩子执行(默认情况下在 `sequence.hooks: 'stack'` 时按逆序) * `beforeEach` 钩子返回的清理函数执行(默认情况下在 `sequence.hooks: 'stack'` 时按逆序) * [`onTestFinished`](/api/hooks#ontestfinished) 回调运行(始终按逆序) * 如果测试失败:[`onTestFailed`](/api/hooks#ontestfailed) 回调运行 * 注意:如果设置了 `repeats` 或 `retry`,则所有这些步骤都会再次执行 6. **[`afterAll`](/api/hooks#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` 块时,钩子遵循层次模式。`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')) }) // 输出: // 外层 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.concurrent` 或 [`sequence.concurrent`](/config/sequence#sequence-concurrent) 时: * 同一文件内的测试可以并行运行 * 每个并发测试仍然运行其自己的 `beforeEach` 和 `afterEach` 钩子 * 使用 [测试上下文](/guide/test-context) 进行并发快照:`test.concurrent('name', async ({ expect }) => {})` ### 6. 报告阶段 在整个测试运行过程中,报告器接收生命周期事件并显示结果。 **发生什么:** * 报告器会在测试运行过程中接收事件 * 收集并格式化结果 * 生成测试摘要 * 如果启用了覆盖率,则生成覆盖率报告 有关报告器生命周期的详细信息,请参阅 [报告器](/api/advanced/reporters) 指南。 ### 7. 全局清理阶段 所有测试完成后,全局清理函数执行。 **发生什么:** * 运行 [`globalSetup`](/config/globalsetup) 文件中的 `teardown()` 函数 * 多个清理函数按其设置顺序的**逆序**运行 * 在 watch 模式下,清理会在进程退出前运行,而不是在测试重新运行之间执行 **范围:** 主进程 ```ts [globalSetup.ts] 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 模式下,生命周期会重复,但有一些区别: 1. **初次运行:** 按照上述说明执行完整生命周期 2. **文件变更时:** * 开始新的[测试运行](/api/advanced/reporters#ontestrunstart) * 仅重新运行受影响的测试文件 * 为这些测试文件再次运行[设置文件](/config/setupfiles) * [全局设置](/config/globalsetup)**不会**重新运行(请使用 [`project.onTestsRerun`](/config/globalsetup#handling-test-reruns) 处理重新运行时的逻辑) 3. **退出时:** * 执行全局清理 * 终止进程 ## 性能考量 了解生命周期有助于优化测试性能: * **全局设置** 适合用于开销较大的一次性操作(数据库填充、启动服务器) * **设置文件** 会在每个测试文件之前运行;如果测试文件很多,请避免在这里执行繁重操作 * 对于不需要隔离的高开销设置,**`beforeAll`** 优于 `beforeEach` * **禁用[隔离](/config/isolate)** 可以提升性能,但设置文件仍会在每个文件之前执行 * **[池配置](/config/pool)** 会影响并行化和可用的 API 提升性能的技巧请参阅[提高性能](/guide/improving-performance)指南。 ## 相关文档 * [全局设置配置](/config/globalsetup) * [设置文件配置](/config/setupfiles) * [测试顺序选项](/config/sequence) * [隔离配置](/config/isolate) * [池配置](/config/pool) * [扩展报告器](/guide/advanced/reporters) - 报告器生命周期事件 * [测试 API 参考](/api/hooks) - 钩子 API