Vitest Hooks 完全指南:测试生命周期钩子的使用、顺序与源码级原理
发布时间:2026/9/13 12:17:37 锦皓数字建站

Vitest Hooks 完全指南测试生命周期钩子的使用、顺序与源码级原理【免费下载链接】vitestNext generation testing framework powered by Vite.项目地址: https://gitcode.com/GitHub_Trending/vi/vitestVitest 提供了一组用于接入测试生命周期的 Hooks 函数beforeEach、afterEach、beforeAll、afterAll、aroundEach、aroundAll、onTestFinished、onTestFailed帮助你在测试前后统一执行 setup 与 teardown 逻辑避免重复样板代码。本文以 Vitest 仓库中 docs/api/hooks.md 为主线结合 hooks.ts 与 run.ts 的源码实现系统讲解每个钩子的签名、作用域、超时配置、执行顺序sequence.hooks以及它们在测试、套件与并发场景下的实际行为读完即可在自己的测试项目中正确编排生命周期逻辑。Hooks 的基本概念与作用域Hooks 允许你接入测试的生命周期从而避免重复的 setup 和 teardown 代码。它们作用于当前上下文在文件顶层使用时作用于整个文件File 级别在describe块内使用时作用于当前 suite。需要注意的是当 Vitest 以 类型检查器 模式运行时typecheck.enabled这些 Hooks不会被调用——因为类型检查并不真正执行测试体。默认情况下测试钩子以栈stack顺序调用即 before 类钩子按注册顺序执行而 after 类钩子按注册顺序的逆序执行。该行为可以通过sequence.hooks配置项调整详见下文「执行顺序sequence.hooks」一节。从源码看所有钩子都通过getCurrentSuite().on(name, hook)注册到当前 suite 上见 hooks.ts运行时再由 runner 从对应 suite 中取出并按序调用。钩子回调若返回 PromiseVitest 会等待其 resolve 后再继续此外注册时会对回调做assertTypes校验传入非函数会直接报错。beforeEach 与 afterEach每个测试的前置与清理beforeEachfunction beforeEach( body: (context: TestContext) unknown, timeout?: number, ): void注册一个回调在当前 suite 中每个测试运行前被调用。如果函数返回 PromiseVitest 会等待其 resolve 后才开始运行测试。可选地你可以传入一个timeout毫秒指定等待多长时间后终止。默认值为 10 秒可通过全局配置hookTimeout修改。import { beforeEach } from vitest beforeEach(async () { // 在每个测试运行前清除 mock 并添加一些测试数据 await stopMocking() await addUser({ name: John }) })这里的beforeEach保证了每个测试运行前用户数据已就绪。beforeEach还可以返回一个可选的清理函数其语义类似于afterEach唯一的区别是它会在所有其他afterEach钩子执行完之后才运行import { beforeEach } from vitest beforeEach(async () { // 每个测试运行前调用一次 await prepareSomething() // 清理函数每个测试运行后调用一次位于所有 afterEach 钩子之后 return async () { await resetSomething() } })这一机制在源码中由getBeforeHookCleanupCallback实现hooks.ts当钩子返回值为函数类型时会将其包装进带超时的清理回调beforeEach注册时还通过CLEANUP_TIMEOUT_KEY单独记录清理阶段的超时时间并保留原始堆栈以辅助报错定位。afterEachfunction afterEach( body: (context: TestContext) unknown, timeout?: number, ): void注册一个回调在当前 suite 中每个测试完成后被调用。如果函数返回 PromiseVitest 会等待其 resolve 后再继续。同样支持可选的timeout毫秒默认 10 秒可全局配置。import { afterEach } from vitest afterEach(async () { await clearTestingData() // 每个测试运行后清除测试数据 })提示你还可以在测试执行过程中使用onTestFinished在测试结束后清理状态详见后文。关于父级 suite 的传递beforeEach/afterEach会沿父链向上传递执行。在callSuiteHook的实现中run.tsbeforeEach会先递归执行父 suite 的钩子再执行当前 suite 的而afterEach则相反先执行当前 suite 的再递归执行父 suite 的从而保证「外层先 setup、后 teardown」的语义。beforeAll 与 afterAll整个套件的开始与收尾beforeAllfunction beforeAll( body: (context: ModuleContext) unknown, timeout?: number, ): void注册一个回调在当前 suite 中所有测试开始运行前被调用一次。如果函数返回 PromiseVitest 会等待其 resolve 后再运行测试。可选timeout毫秒默认 10 秒可全局配置。import { beforeAll } from vitest beforeAll(async () { await startMocking() // 所有测试运行前调用一次 })beforeAll同样可以返回清理函数语义类似afterAll区别在于它会在所有其他afterAll钩子执行完之后才运行import { beforeAll } from vitest beforeAll(async () { // 所有测试运行前调用一次 await startMocking() // 清理函数所有测试运行后调用一次位于所有 afterAll 钩子之后 return async () { await stopMocking() } })afterAllfunction afterAll( body: (context: ModuleContext) unknown, timeout?: number, ): void注册一个回调在当前 suite 中所有测试运行完成后被调用一次。如果函数返回 PromiseVitest 会等待其 resolve 后再继续。可选timeout毫秒默认 10 秒可全局配置。import { afterAll } from vitest afterAll(async () { await stopMocking() // 该方法会在所有测试运行后被调用 })从 run.ts 可以看出afterAll被放在finally块中执行——即使beforeAll或某个子测试失败afterAll依然会被调用确保资源总能被释放同时beforeAll返回的清理函数会在afterAll之后统一执行。aroundEach将每个测试包裹进一个上下文function aroundEach( body: ( runTest: () Promisevoid, context: TestContext, ) Promisevoid, timeout?: number, ): void注册一个回调包裹当前 suite 中的每个测试。回调接收一个runTest函数你必须调用它来真正运行测试。runTest()会依次执行beforeEach钩子、测试本身、测试中访问的 fixtures以及afterEach钩子。在aroundEach回调中访问的 fixtures会在runTest()被调用之前完成初始化并在 aroundEach 的 teardown 代码完成后被销毁因此你可以在 setup 与 teardown 两个阶段安全地使用它们。警告你必须在回调中调用runTest()。如果runTest()未被调用测试将报错失败。可选地你可以提供timeout毫秒。该超时分别独立地作用于 setup 阶段runTest()之前与 teardown 阶段runTest()之后。默认 10 秒可全局配置。import { aroundEach, test } from vitest aroundEach(async (runTest) { await db.transaction(runTest) }) test(insert user, async () { await db.insert({ name: Alice }) // 测试结束后事务被自动回滚 })何时使用aroundEach当你的测试需要运行在包裹它的上下文中时使用aroundEach例如将测试包裹进 AsyncLocalStorage 上下文用 tracing spans 包裹测试数据库事务如果只是需要在测试前后运行代码优先使用beforeEach 返回清理函数的方式beforeEach(async () { await database.connect() return async () { await database.disconnect() } })多个 aroundEach 钩子的嵌套当注册了多个aroundEach钩子时它们会互相嵌套第一个注册的钩子位于最外层aroundEach(async (runTest) { console.log(outer before) await runTest() console.log(outer after) }) aroundEach(async (runTest) { console.log(inner before) await runTest() console.log(inner after) }) // 输出顺序 // outer before // inner before // test // inner after // outer after这一行为在 e2e 测试 around-each.test.ts 中有直接验证其断言输出恰好为outer before → inner before → test → inner after → outer after。上下文与 Fixtures回调的第二个参数是测试上下文TestContext这意味着你可以在aroundEach中使用 fixturesimport { aroundEach, test as base } from vitest const test base.extend{ db: Database; user: User }({ db: async ({}, use) { // db 在 aroundEach 钩子之前创建 const db await createTestDatabase() await use(db) await db.close() }, user: async ({ db }, use) { // user 作为事务的一部分运行 // 因为它在 test 内部被访问 const user await db.createUser() await use(user) }, }) // 注意aroundEach 也挂载在 test 上 // 以便获得更好的 TypeScript 类型支持 test.aroundEach(async (runTest, { db }) { await db.transaction(runTest) }) test(insert user, async ({ db, user }) { await db.insert(user) })源码层面aroundEach的实现hooks.ts通过withFixtures将 fixtures 注入回调并经由callAroundEachHooksrun.ts在调用runTest之前取得 fixture 清理的检查点getFixtureCleanupCount从而区分「为 aroundEach 创建的 fixtures」与「测试内部创建的 fixtures」确保各自在正确的时机被清理。aroundAll将整个套件包裹进一个上下文function aroundAll( body: ( runSuite: () Promisevoid, context: ModuleContext, ) Promisevoid, timeout?: number, ): void注册一个回调包裹当前 suite 中的所有测试。回调接收一个runSuite函数你必须调用它来运行套件内的测试。runSuite()会运行套件内的所有测试包括beforeAll/afterAll/beforeEach/afterEach钩子、aroundEach钩子以及 fixtures。警告你必须在回调中调用runSuite()。如果runSuite()未被调用该钩子会报错失败并且套件内的所有测试都会被跳过。这一点在 run.ts 中有对应实现当钩子执行完毕但use即runSuite未被调用时会抛出AroundHookSetupError而在 runSuite 的 catch 分支中若 suite 从未真正运行!suiteRan会调用markTasksAsSkipped将全部任务标记为 skip。可选地你可以提供timeout毫秒。该超时分别独立地作用于 setup 阶段runSuite()之前与 teardown 阶段runSuite()之后。默认 10 秒可全局配置。import { aroundAll, test } from vitest aroundAll(async (runSuite) { await tracer.trace(test-suite, runSuite) }) test(test 1, () { // 在 tracing span 内运行 }) test(test 2, () { // 也在同一个 tracing span 内运行 })何时使用aroundAll当你的套件需要运行在包裹所有测试的上下文中时使用aroundAll例如将整个套件包裹进 AsyncLocalStorage 上下文用 tracing spans 包裹套件数据库事务如果只是需要在所有测试前后各运行一次代码优先使用beforeAll 返回清理函数的方式beforeAll(async () { await server.start() return async () { await server.stop() } })多个 aroundAll 钩子的嵌套多个aroundAll钩子同样互相嵌套第一个注册的位于最外层aroundAll(async (runSuite) { console.log(outer before) await runSuite() console.log(outer after) }) aroundAll(async (runSuite) { console.log(inner before) await runSuite() console.log(inner after) }) // 输出顺序outer before → inner before → tests → inner after → outer after套件间的嵌套关系每个 suite 拥有自己独立的aroundAll钩子父 suite 的aroundAll会包裹子 suite 的执行import { AsyncLocalStorage } from node:async_hooks import { aroundAll, describe, test } from vitest const context new AsyncLocalStorage{ suiteId: string }() aroundAll(async (runSuite) { await context.run({ suiteId: root }, runSuite) }) test(root test, () { // context.getStore() 返回 { suiteId: root } }) describe(nested, () { aroundAll(async (runSuite) { // 在这里可以访问父级上下文 await context.run({ suiteId: nested }, runSuite) }) test(nested test, () { // context.getStore() 返回 { suiteId: nested } }) })测试级 HooksonTestFinished 与 onTestFailedVitest 还提供了几个可以在测试执行过程中调用的钩子用于在测试结束时清理状态。警告这些钩子如果在测试体外被调用会抛出错误。源码createTestHookhooks.ts通过getCurrentTest()检查当前是否存在运行中的测试不存在时抛出Hook onTestFinished() can only be called inside a test。onTestFinished该钩子在测试运行结束后总是被调用无论测试通过还是失败。它位于afterEach钩子之后调用——因为afterEach可能影响测试结果。它接收的TestContext对象与beforeEach/afterEach相同。import { onTestFinished, test } from vitest test(performs a query, () { const db connectDb() onTestFinished(() db.close()) db.query(SELECT * FROM users) })警告如果测试以并发方式运行test.concurrent你应该始终从测试上下文中获取onTestFinished因为 Vitest 不会在全局钩子中追踪并发测试import { test } from vitest test.concurrent(performs a query, ({ onTestFinished }) { const db connectDb() onTestFinished(() db.close()) db.query(SELECT * FROM users) })该钩子在创建可复用逻辑时尤其有用——比如把一个返回测试数据库连接的辅助函数单独抽离到文件中让每个测试无需关心关闭连接// 这个可以放在单独的文件中 function getTestDb() { const db connectMockedDb() onTestFinished(() db.close()) return db } test(performs a user query, async () { const db getTestDb() expect( await db.query(SELECT * from users).perform() ).toEqual([]) }) test(performs an organization query, async () { const db getTestDb() expect( await db.query(SELECT * from organizations).perform() ).toEqual([]) })它也是清理 spy 的良好实践避免 spy 泄漏到其他测试。你可以通过全局启用restoreMocks配置或者在onTestFinished内恢复 spy——如果在测试末尾直接恢复 mock一旦某个断言失败恢复代码就不会执行使用onTestFinished则能确保代码总是运行import { onTestFinished, test } from vitest test(performs a query, () { const spy vi.spyOn(db, query) onTestFinished(() spy.mockClear()) db.query(SELECT * FROM users) expect(spy).toHaveBeenCalled() })提示onTestFinished钩子总是以逆序调用且不受sequence.hooks配置的影响。从源码 run.ts 可以看到它通过callTestHooks(runner, test, test.onFinished!, stack)以固定的stack模式调用与全局序列配置无关。onTestFailed该钩子仅在测试失败后被调用。它同样位于afterEach钩子之后调用因为afterEach可能影响测试结果接收TestContext对象。该钩子主要用于调试import { onTestFailed, test } from vitest test(performs a query, () { const db connectDb() onTestFailed(({ task }) { console.log(task.result.errors) }) db.query(SELECT * FROM users) })警告如果测试以并发方式运行你应该始终从测试上下文中获取onTestFailedimport { test } from vitest test.concurrent(performs a query, ({ onTestFailed }) { const db connectDb() onTestFailed(({ task }) { console.log(task.result.errors) }) db.query(SELECT * FROM users) })在源码 run.ts 中onTestFailed的调用发生在onTestFinished之后且只有当测试结果为 fail 时才触发它遵循runner.config.sequence.hooks的序列配置默认stack逆序。超时配置hookTimeout所有钩子的默认超时均为 10 秒毫秒可以通过hookTimeout全局配置类型number默认值Node.js 环境下为10_000当browser.enabled为true时为30_000CLI--hook-timeout10000或--hookTimeout10000设为0可完全禁用超时。在源码中默认值来自getDefaultHookTimeout()hooks.ts即getRunner().config.hookTimeout每个钩子也允许通过第二个参数单独覆盖。对于aroundEach/aroundAll超时通过AROUND_TIMEOUT_KEY记录hooks.ts并由callAroundHooksrun.ts在 setup 与 teardown 两个阶段分别起独立的计时器任一阶段超时都会抛出对应的AroundHookSetupError/AroundHookTeardownError。执行顺序sequence.hooks钩子的执行顺序由sequence.hooks配置项控制类型stack | list | parallel默认值stackCLI--sequence.hooksvalue三种模式的行为模式行为stackafter 钩子按逆序执行before 钩子按定义顺序执行list所有钩子按定义顺序执行parallel同一组钩子并行执行父 suite 的钩子仍先于当前 suite 的钩子运行实际并行数受maxConcurrency限制提示该选项不影响onTestFinished它总是以逆序调用。对应到源码getSuiteHooksrun.ts在stack模式下对afterAll/afterEach执行hooks.slice().reverse()callTestHooksrun.ts在parallel模式下通过Promise.all与limitMaxConcurrency并发执行钩子其余模式则逐个顺序执行。callCleanupHooks对beforeEach/beforeAll返回的清理函数也遵循同样的序列规则run.ts。在配置文件中使用示例import { defineConfig } from vitest/config export default defineConfig({ test: { sequence: { hooks: list, // 让所有钩子都按定义顺序执行 }, }, })或通过 CLI 覆盖npx vitest --sequence.hookslist小结如何选择合适的钩子需求推荐钩子每个测试前准备数据 / 后清理数据beforeEachafterEach或beforeEach返回清理函数整个套件只执行一次 setup / teardownbeforeAllafterAll或beforeAll返回清理函数每个测试需要运行在某个上下文事务、AsyncLocalStorage、trace span中aroundEach整个套件需要运行在某个上下文共享 span、整库事务中aroundAll在测试内部动态注册清理逻辑可复用工具函数、spy 恢复onTestFinished测试失败后的调试与诊断onTestFailed全部 8 个钩子的签名与示例均可直接在 docs/api/hooks.md 查阅其运行时行为可在 hooks.ts注册实现与 run.ts执行调度中追踪验证e2e 行为断言可参考 around-each.test.ts。【免费下载链接】vitestNext generation testing framework powered by Vite.项目地址: https://gitcode.com/GitHub_Trending/vi/vitest创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。