
Vitest 测试运行生命周期完全指南从初始化到全局清理的执行链解析【免费下载链接】vitestNext generation testing framework powered by Vite.项目地址: https://gitcode.com/GitHub_Trending/vi/vitestVitest 从命令行启动到最后一个测试文件结束会经历一条有严格顺序的生命周期流水线初始化配置、全局设置、创建 Worker、执行 setup 文件、收集与运行测试、报告结果、全局清理。本文以 Vitest 官方生命周期文档为主线结合当前仓库的配置文档与源码实现runtime runner、node 主进程完整拆解每个阶段的职责、作用域与执行顺序帮助你写出顺序正确、易于调试、性能更优的测试套件并能准确回答这段代码到底在哪个进程、什么时候执行这类关键问题。生命周期总览七个主阶段一次典型的 Vitest 测试运行会依次经过以下主阶段初始化Initialization加载配置并进行项目级准备全局设置Global Setup在运行任何测试之前执行一次性设置创建 WorkerWorker Creation依据 pool 配置派生出测试 Worker测试文件收集Test File Collection发现并组织测试文件测试执行Test Execution测试与各类 hook、断言按既定顺序运行报告Reporting收集结果并输出报告全局清理Global Teardown所有测试完成后执行最终清理需要特别留意的是阶段 46 会为每个测试文件各自执行一遍。因此对于整个测试套件而言这几个阶段会被反复执行多次当你配置了多于 1 个 Worker 时不同文件的这些阶段还会并行发生。阶段一初始化阶段运行vitest命令后框架首先加载配置并准备测试环境。这一阶段发生的事情解析 命令行参数加载 配置文件校验项目结构触发条件与重复执行当配置文件或其任一被导入的文件发生变化时此阶段会再次执行典型的 watch 模式场景。作用域主进程在任何测试 Worker 创建之前。这意味着初始化阶段做的所有准备都发生在测试代码能够访问任何东西之前——它是整个运行周期中最靠前、也最干净的时机。阶段二全局设置阶段globalSetup如果你配置了globalSetup文件它们会在任何测试 Worker 被创建之前运行一次。这一阶段发生的事情全局设置文件中的setup()函数或导出的default函数按声明顺序依次执行多个全局设置文件按它们在配置中定义的顺序运行作用域主进程与测试 Worker 完全隔离。三条重要注意事项全局设置运行在与测试不同的全局作用域中测试无法访问全局设置中定义的变量如需共享数据请使用provide/inject机制全局设置仅当至少有一个测试排队等待执行时才会运行。典型的全局设置文件如下两种写法等价setup/teardown具名导出或default函数返回 teardownexport function setup(project) { // 在全部测试之前运行一次 console.log(Global setup) // 通过 provide 与测试共享数据 project.provide(apiUrl, http://localhost:3000) } export function teardown() { // 在全部测试之后运行一次 console.log(Global teardown) }深入globalSetup 的配置形态与 provide/inject 数据共享根据 globalSetup 配置文档该选项的类型是string | string[]路径相对于项目 root。setup方法与default函数都会接收一个 TestProject 实例作为第一个参数。多个全局设置文件允许并存setup顺序执行而teardown以逆序执行。由于全局设置与测试运行在不同的全局作用域共享数据必须通过可序列化的provide/inject通道import type { TestProject } from vitest/node export default function setup(project: TestProject) { project.provide(wsPort, 3000) } declare module vitest { export interface ProvidedContext { wsPort: number } }import { inject } from vitest inject(wsPort) 3000provide配置项本身也可直接在vitest.config中静态声明其要求是属性名必须是字符串值必须可被结构化克隆算法序列化因为该对象要在不同进程间传递。若使用 TypeScript可通过declare module vitest增强ProvidedContext接口获得类型安全访问。深入测试重跑rerun时的处理钩子如果你需要为每次测试重跑重新配置全局环境可以使用onTestsRerun钩子——它在 watch 模式下文件变更触发重跑时被调用运行器会等待它完成后才继续执行测试。注意不能以解构方式获取它如{ onTestsRerun }因为它依赖project上下文import type { TestProject } from vitest/node export default function setup(project: TestProject) { project.onTestsRerun(async () { await restartDb() }) }从源码看onTestsRerun回调注册在 node/project.ts 与 node/core.ts 中实现是主进程层面的机制印证了它必须在全局设置作用域内使用的事实。阶段三创建 Worker 阶段全局设置完成后Vitest 根据你的 pool 配置 创建测试 Worker。这一阶段发生的事情根据browser.enabled或pool设置threads、forks、vmThreads或vmForks派生出 Worker每个 Worker 拥有自己的隔离环境除非禁用了 isolation默认情况下 Worker 不会被复用以提供隔离。Worker 仅在下述情形下被复用isolation 被禁用或 pool 为vmThreads/vmForks因为 Node.js VM 本身提供了足够的隔离。作用域Worker 进程/线程。深入四种 pool 的选择pool 配置文档详细对比了四种池的实现差异Pool底层机制特点threadsworker_threads多线程进程相关 API如process.chdir()不可用process.env.TZ修改不影响时区Prisma、bcrypt、canvas 等原生库可能段错误forks默认child_process与主进程通信略慢但进程类 API 可用vmThreadsworker_threads VM 沙箱更快但 ESM 下 VM 模块不稳定、会内存泄漏由 vmMemoryLimit 触发 Worker 重启来对抗Worker 回收昂贵vmForkschild_process VM 沙箱进程类 API 可用回收超限 Worker 只需让子进程退出大套件下通常比vmThreads更快vm 池还要注意沙箱内原生模块抛出的错误其Error构造函数与测试环境中的不同err instanceof Error可能为falseESM 缓存无法清除沙箱中访问全局变量更慢。Worker 数量的上限由 maxWorkers 控制非 watch 模式默认使用全部可用并行度watch 模式默认使用一半支持数字或百分比字符串如50%内部基于os.availableParallelism计算。阶段四测试文件设置阶段setupFiles在每个测试文件运行之前setup 文件 都会被执行。这一阶段发生的事情setup 文件与测试运行在同一个进程中默认情况下 setup 文件并行执行可通过sequence.setupFiles配置为list串行setup 文件在每个测试文件之前执行任何全局状态或配置都可以在此初始化。作用域Worker 进程与测试相同。两条重要注意事项如果 isolation 被禁用setup 文件仍然会在每个测试文件之前重新执行以触发副作用但导入的模块会被缓存——你访问到的是同一个全局对象。因此要避免重复做不必要的工作可用globalThis标志位做幂等保护参见 setupFiles 配置文档 中的示例在 watch 模式下编辑 setup 文件会触发所有测试重跑。一个典型的 setup 文件示例import { afterEach } from vitest // 在每个测试文件之前运行 console.log(Setup file executing) // 注册适用于所有测试的 hook afterEach(() { cleanup() })深入setupFiles 与 globalSetup 的本质区别setupFiles 配置文档 特别强调setup 文件与测试同进程运行、Vitest 会忽略其中的任何导出而globalSetup在主线程中只运行一次。选择依据很简单——如果你需要与测试共享同一进程上下文如注册全局 polyfill、扩展expect用setupFiles如果需要一次性的昂贵准备工作如启动数据库、拉起服务用globalSetup。另外若你在 setup 文件中运行后台重任务可以通过process.env.VITEST_POOL_ID整数字符串区分 Worker 以分摊负载。阶段五测试收集与执行阶段这是测试真正运行的主阶段。测试文件的执行顺序测试文件依据配置执行在单个 Worker 内默认按顺序执行跨不同 Worker 的文件并行运行并行度由maxWorkers配置可通过sequence.shuffle随机化顺序或用sequence.sequencer自定义排序除非启用 shuffle长时间运行的测试通常会先启动基于缓存排序。这是因为 Vitest 用缓存记录各文件的运行耗时让慢文件优先调度从而缩短总耗时而开启 shuffle 后你会失去这一性能收益但能暴露测试间意外的前后依赖。单个测试文件内部的执行顺序每个测试文件的执行严格遵循以下顺序文件级代码describe块之外的所有代码立即执行收集阶段测试收集describe块被处理测试作为导入测试文件的副作用被注册aroundAllhooks包裹套件内的所有测试必须调用runSuite()beforeAllhooks在套件内任何测试之前运行一次每个测试依次执行aroundEachhooks 包裹测试必须调用runTest()beforeEachhooks 执行按定义顺序或依sequence.hooks配置测试函数执行afterEachhooks 执行默认sequence.hooks: stack时按逆序beforeEachhook 返回的清理函数执行默认stack时同样逆序onTestFinished回调运行始终逆序若测试失败onTestFailed回调运行注意若设置了repeats或retry上述所有步骤会重复执行afterAllhooks套件内所有测试完成后运行一次beforeAllhook 返回的清理函数套件内所有测试完成后运行一次。完整执行流程示例// 收集阶段立即执行 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 hooks 之后运行一次 console.log(beforeAllCleanup) } }) aroundEach(async (runTest) { // 包裹每个测试 console.log(aroundEach before) await runTest() console.log(aroundEach after) }) beforeEach(() { // 在每个测试之前运行 console.log(beforeEach) return function beforeEachCleanup() { // 在 afterEach hooks 之后运行 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深入hook 的嵌套与清理链从 Hooks API 可以提炼出几个容易被忽视的细节beforeEach返回的清理函数与afterEach等价唯一区别是它在所有其他afterEach之后执行beforeAll返回的清理函数同理在所有其他afterAll之后执行多个aroundEach/aroundAll互相嵌套先注册的在外层。例如两个aroundEach的输出为outer before → inner before → test → inner after → outer afteraroundEach/aroundAll必须调用runTest()/runSuite()否则测试会报错失败aroundAll未调用时整个套件的测试会被跳过aroundEach适合需要把测试包在某个上下文中执行的场景AsyncLocalStorage 上下文、tracing span、数据库事务等。aroundEach还能拿到测试上下文作为第二个参数因此可以与test.extend的 fixtures 配合使用参考 Hooks API 中的 fixtures 示例所有 hook 的超时默认 10 秒浏览器环境为 30 秒可通过hookTimeout全局配置0表示禁用超时或在单个 hook 上以第二个参数传入毫秒数。嵌套套件Nested Suites使用嵌套describe块时hook 遵循层级模式aroundAll与aroundEach分别包裹各自作用域父级 hook 包裹子级 hookdescribe(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可以看到清晰的洋葱模型before系列由外向内逐层深入after系列由内向外逐层退出。这也是新手最容易犯错的区域——把 hook 写错层级就会导致清理顺序与预期不符。并发测试Concurrent Tests使用test.concurrent或sequence.concurrent时同一文件内的测试可以并行运行每个并发测试仍然独立运行自己的beforeEach与afterEachhooks并发快照场景请使用 测试上下文 中的expecttest.concurrent(name, async ({ expect }) {})。关于并发还有一个实践要点由于 Vitest 的全局 hook 不追踪并发测试在test.concurrent中应始终使用测试上下文解构出的onTestFinished/onTestFailed进行资源清理与失败调试而不是全局导入的版本参见 Hooks API 中的并发警示。阶段六报告阶段在整个测试运行期间reporter 接收生命周期事件并展示结果。这一阶段发生的事情reporter 随测试推进接收事件结果被收集并格式化生成测试摘要生成覆盖率报告若启用。关于 reporter 生命周期事件的细节参见 Reporters 指南。从实现上看reporter 的各个回调如onTestRunStart、onTestRunEnd对应着生命周期中不同时点的事件流这也是自定义 reporter 时判断哪个阶段该做什么的依据。阶段七全局清理阶段所有测试完成后全局清理函数执行。这一阶段发生的事情globalSetup文件中的teardown()函数运行多个清理函数按 setup 的逆序运行在 watch 模式下清理在进程退出前执行而不是在每次测试重跑之间。作用域主进程。export function teardown() { // 清理全局资源 console.log(Global teardown complete) }生命周期在不同作用域中的分布理解代码在哪里执行是避免常见坑的关键。下表汇总了各阶段的作用域、测试上下文访问权与运行次数阶段作用域可访问测试上下文运行次数配置文件主进程❌ 否每次 Vitest 运行一次全局设置主进程❌ 否用provide/inject每次 Vitest 运行一次Setup 文件Worker与测试相同✅ 是每个测试文件之前文件级代码Worker✅ 是每个测试文件一次aroundAllWorker✅ 是每个套件一次包裹所有测试beforeAll/afterAllWorker✅ 是每个套件一次aroundEachWorker✅ 是每个测试包裹每个测试beforeEach/afterEachWorker✅ 是每个测试测试函数Worker✅ 是一次retry/repeats 时多次全局清理主进程❌ 否每次 Vitest 运行一次这个表格揭示了核心设计凡是在主进程运行的代码都与测试隔离凡是在 Worker 运行的代码才能与测试共享状态。忘记这一点往往会导致变量在全局设置里定义了但测试里取不到的困惑。Watch 模式下的生命周期在 watch 模式下生命周期会循环执行但有几处差异首次运行执行完整的上述生命周期文件变更时开始新一轮 test run仅重跑受影响的测试文件这些测试文件的 setup 文件 会再次运行globalSetup不会重新执行重跑相关逻辑请使用project.onTestsRerun退出时全局清理执行进程终止。理解这一点对调试很有价值如果你在globalSetup里启动了数据库/服务并期望每次重跑都重置状态watch 模式下它不会重跑——正确做法是把重置逻辑放进onTestsRerun回调。生命周期视角下的性能优化理解生命周期有助于优化测试性能globalSetup适合昂贵的一次性操作数据库播种、服务启动——它只运行一次分摊成本最低setup 文件在每个测试文件之前运行——如果你有大量测试文件避免在其中做重操作beforeAll优于beforeEach适用于不需要隔离的昂贵准备beforeEach在每个测试前都跑一遍禁用 isolation可以提升性能但注意 setup 文件仍然会在每个文件之前执行Pool 配置影响并行化程度与可用的进程/线程 API如threads下无法使用process.chdir()。从仓库的示例项目也可以看到生命周期在实践中的组合使用examples/basic/test/basic.test.ts 与 examples/basic/test/suite.test.ts 展示了文件内 hook 与断言的基本编排而 examples/profiling/global-setup.ts 则展示了全局设置阶段的实战用法。更多优化建议可阅读 Improving Performance 指南。总结Vitest 的生命周期可以概括为一条主进程打底、Worker 反复、主进程收尾的执行链初始化与全局设置在主进程完成一次性准备setup 文件、文件级代码与各类 hook 在Worker内围绕每个测试文件循环执行报告与全局清理再回到主进程收束。把握住作用域与顺序两个维度——aroundAll/aroundEach包裹、before系列由外向内、after系列由内向外、onTestFinished始终逆序——你就能准确预测任何测试文件的执行轨迹写出既高效又不易踩坑的测试套件。相关文档导航Global Setup 配置Setup Files 配置测试排序选项 sequence隔离配置 isolatePool 配置Hooks API 参考 —— 各 hook 的签名与行为provide/inject 数据共享Setup and Teardown 教程 —— hook 的实战入门扩展 Reporter —— reporter 生命周期事件【免费下载链接】vitestNext generation testing framework powered by Vite.项目地址: https://gitcode.com/GitHub_Trending/vi/vitest创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考