深入解析 Vitest 生命周期钩子:beforeEach / aroundEach / test.extend 在 Supabase 仓库中的实战用法
本篇基于仓库内置的 Vitest 技能参考文档 core-hooks.md(基于 Vitest 3.x,由 SKILL.md 声明)展开,完整覆盖 beforeAll / afterAll / beforeEach / afterEach 四大基础钩子、清理函数返回模式、作用域钩子、钩子超时、around* 环绕钩子、测试级钩子(onTestFinished / onTestFailed)以及 test.extend 扩展钩子的类型感知特性,并结合 monorepo 中 packages/pg-meta、packages/common、apps/studio、e2e/studio 的真实测试代码印证每一种模式的落地写法。读完后你将掌握:如何在数据库类测试中正确管理 setup/teardown 生命周期、如何用返回清理函数和环绕钩子避免资源泄漏、以及钩子执行顺序(stack/list/parallel)的配置原理。
一、四大基础钩子:文件级与用例级生命周期
Vitest 提供两组钩子:beforeAll / afterAll 在整个文件(或 suite)级别各执行一次,beforeEach / afterEach 在每个 test 用例执行前后各执行一次。文档给出的标准导入与用法如下:
import { afterAll, afterEach, beforeAll, beforeEach, test } from 'vitest'
beforeAll(async () => {
// Runs once before all tests in file/suite
await setupDatabase()
})
afterAll(async () => {
// Runs once after all tests in file/suite
await teardownDatabase()
})
beforeEach(async () => {
// Runs before each test
await clearTestData()
})
afterEach(async () => {
// Runs after each test
await cleanupMocks()
})
仓库实证:pg-meta 的 beforeAll/afterAll 数据库清理模式
packages/pg-meta 是仓库中与 Postgres 元数据交互的包,它的测试文件几乎统一采用 beforeAll + afterAll 包住整个数据库生命周期。以 extensions.test.ts 为例:
import { afterAll, beforeAll, expect, test } from 'vitest'
import { cleanupRoot, createTestDatabase } from './db/utils'
beforeAll(async () => {
// Any global setup if needed
})
afterAll(async () => {
await cleanupRoot()
})
beforeAll 承担一次性全局初始化(建库、加载依赖),afterAll 统一调用 cleanupRoot() 清理测试数据库,保证测试进程退出前不留残余资源。同目录下 views.test.ts、roles.test.ts、columns.test.ts 等文件均是同一范式。
值得注意的是,pg-meta 测试对“每个用例独立的测试数据库”采用了比 beforeEach 更精确的包装函数方案(见 extensions.test.ts):
const withTestDatabase = (
name: string,
fn: (db: Awaited<ReturnType<typeof createTestDatabase>>) => Promise<void>
) => {
test(name, async () => {
const db = await createTestDatabase()
try {
await fn(db)
} finally {
await db.cleanup()
}
})
}
从源码结构看,这种包装函数将“建库—执行—finally 清理”固化为一个可复用模板,语义上等价于文档第二节的“返回清理函数”模式,但把清理逻辑收敛到了用例内部,即使断言失败也不会跳过 db.cleanup()。
仓库实证:common 包的 beforeEach/afterEach 状态重置
单元测试中最常见的用途是状态重置。safe-storage.test.ts 在每个用例前用 beforeEach 构造干净的内存 storage,用 afterEach 统一复位(该文件同时展示了 // @vitest-environment jsdom 文件头注释切环境的用法):
// @vitest-environment jsdom
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
// ...
beforeEach(() => {
// 为每个用例准备全新的内存 storage 实例
})
afterEach(() => {
// 复位 vi 定时器 / mock 状态
})
这套模式与 vitest.setup.ts 所在的 packages/common 各测试文件(如 consent-state.test.ts、telemetry-first-touch-store.test.ts)保持一致,是 beforeEach/afterEach 最典型的落地场景:让每个用例看到相同初始状态。
二、清理函数返回模式:用 return 替代 after*
当 setup 产生了需要配对释放的资源(服务器、连接)时,为每个资源单独写一个 afterAll/afterEach 容易遗漏或重复。Vitest 允许 before* 钩子直接返回一个清理函数,框架会在对应的 after* 时机自动调用:
beforeAll(async () => {
const server = await startServer()
// Returned function runs as afterAll
return async () => {
await server.close()
}
})
beforeEach(async () => {
const connection = await connect()
// Runs as afterEach
return () => connection.close()
})
该模式的核心价值是资源与其清理逻辑在同一个函数体内声明,setup 时拿到的句柄(server、connection)通过闭包直接传给清理函数,天然规避了“在 after 钩子里重复创建或找不到实例”的问题。文档 Key Points 也明确建议:“Return cleanup function from before* to avoid after* duplication”。
这一思想在 e2e 层有对应体现:e2e/studio/utils/test.ts 提供了 withSetupCleanup 工具,配合 TS 的 using 关键字返回 Symbol.asyncDispose 对象,实现“无论测试成败都执行 cleanup”:
export const withSetupCleanup = async (
setup: () => Promise<void>,
cleanup: () => Promise<void>
) => {
await setup()
return {
async [Symbol.asyncDispose]() {
await cleanup()
},
}
}
从源码结构看,它与 Vitest 的 return-cleanup 模式属于同一设计哲学:把“清理时机”交给运行时(框架 / 语言运行时)保证,而不是依赖测试作者的自觉。
三、作用域钩子:外层 describe 钩子向下穿透
钩子不是文件级的全局变量,它绑定在注册它的那个 suite 上,并对该 suite 及其嵌套子 suite 生效。执行顺序为“外层先执行,内层后执行”:
describe('outer', () => {
beforeEach(() => console.log('outer before'))
test('test 1', () => {}) // outer before → test
describe('inner', () => {
beforeEach(() => console.log('inner before'))
test('test 2', () => {}) // outer before → inner before → test
})
})
这意味着在 monorepo 中拆分大型测试文件时,可以在顶层 describe 中放置跨全部子分组共享的重型 setup(例如连接共享 fixture),在内层 describe 中仅叠加该分组专属的轻量 setup,既避免重复代码又不会让无关分组承担不必要的初始化开销。
四、钩子超时:为慢 setup 单独放宽时限
钩子与测试一样有超时约束,且可单独配置——第二个参数传入毫秒数:
beforeAll(async () => {
await slowSetup()
}, 30_000) // 30 second timeout
这对需要启动真实数据库容器、加载大型 fixture 的 beforeAll 尤其有用:全局测试超时可以保持严格以快速暴露死循环,而个别重量级 setup 通过钩子级超时参数显式放宽,而不是把整个 suite 的超时调松。apps/studio 的 vitest.config.ts 展示了配置层面的稳定性策略与之互补:retry: IS_CI ? 2 : 0(CI 重试 flaky 测试、本地失败立即暴露)——超时参数管“单个钩子允许跑多久”,配置项管“失败后重试几轮”,二者共同构成稳定性预算。
五、Around 钩子:洋葱模型环绕测试
aroundEach / aroundAll 是 Vitest 3.x 引入的环绕钩子:钩子接收一个 runTest()(或 runSuite())回调,回调之前是 setup,之后是 teardown,形成对测试体的“包裹”:
import { aroundEach, test } from 'vitest'
// Wrap each test in database transaction
aroundEach(async (runTest) => {
await db.beginTransaction()
await runTest() // Must be called!
await db.rollback()
})
test('insert user', async () => {
await db.insert({ name: 'Alice' })
// Automatically rolled back after test
})
aroundAll:包裹整个 suite
import { aroundAll, test } from 'vitest'
aroundAll(async (runSuite) => {
console.log('before all tests')
await runSuite() // Must be called!
console.log('after all tests')
})
多个 around 钩子的嵌套顺序
注册多个 around 钩子时按洋葱层嵌套,执行顺序为“外层 setup → 内层 setup → 测试 → 内层 teardown → 外层 teardown”:
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')
})
// Order: outer before → inner before → test → inner after → outer after
around 相比 before/after 的独特能力
- 必须显式调用
runTest()/runSuite():忘记调用则测试根本不会执行,框架会以错误暴露,防止“注册了包裹但漏写回调”的静默失败; - teardown 与 setup 闭包共享状态:与返回清理函数模式相同,但额外允许在 teardown 中感知 setup 阶段的中间值(如事务 ID、连接的运行状态);
- 可条件性执行后续步骤:由于 setup 与 teardown 处于同一异步函数体内,可以写成
try { await runTest() } finally { rollback() },即使测试抛错也保证回滚——这正是数据库事务包裹用例最需要的语义。
六、测试级钩子:onTestFinished 与 onTestFailed
作用在测试体内部的钩子,用于清理在测试运行过程中动态创建的资源:
import { onTestFailed, onTestFinished, test } from 'vitest'
test('with cleanup', () => {
const db = connect()
// Runs after test finishes (pass or fail)
onTestFinished(() => db.close())
// Only runs if test fails
onTestFailed(({ task }) => {
console.log('Failed:', task.result?.errors)
})
db.query('SELECT * FROM users')
})
onTestFinished:无论用例通过还是失败都执行,是资源释放的首选挂载点;onTestFailed:仅在用例失败时触发,接收{ task }参数,可用于打印错误上下文、抓取诊断信息。
可复用的清理包装:useTestDb 模式
文档给出的“资源工厂”模式,让资源创建与自动清理封装进一个普通函数:
function useTestDb() {
const db = connect()
onTestFinished(() => db.close())
return db
}
test('query users', () => {
const db = useTestDb()
expect(db.query('SELECT * FROM users')).toBeDefined()
})
test('query orders', () => {
const db = useTestDb() // Fresh connection, auto-closed
expect(db.query('SELECT * FROM orders')).toBeDefined()
})
每个用例调用 useTestDb() 都获得一条全新连接,且清理已随注册自动绑定——用例代码里不再出现 try/finally。该模式与 packages/pg-meta 的 withTestDatabase 包装函数互为表里:一个靠框架钩子保证清理,一个靠 finally 保证清理,可按资源特性选型。
并发测试中的上下文钩子
对 test.concurrent 用例,模块级的 onTestFinished 不适用,应使用测试上下文(t 参数)中提供的钩子:
test.concurrent('concurrent', ({ onTestFinished }) => {
const resource = allocate()
onTestFinished(() => resource.release())
})
从源码结构看,这符合“并发用例共享执行流,必须通过各自上下文注册清理”的约束:apps/studio 的 vitest.config.ts 与 e2e/studio 的 Playwright 用例均属于可能并行的场景,编写此类测试时应一律走上下文钩子。
七、test.extend 与类型感知钩子
通过 test.extend 声明自定义 fixture 后,挂在测试对象上的 beforeEach / afterEach 会获得 fixture 的类型推导:
const test = base.extend<{ db: Database }>({
db: async ({}, use) => {
const db = await createDb()
await use(db)
await db.close()
},
})
// These hooks know about `db` fixture
test.beforeEach(({ db }) => {
db.seed()
})
test.afterEach(({ db }) => {
db.clear()
})
关键点:
- fixture 函数本身遵循“
use(x)之前创建、之后清理”的约定,与 around 钩子同构; test.beforeEach(({ db }) => ...)的回调参数被类型系统推断为{ db: Database },拼错 fixture 名或误用返回值会在编译期报错,而不是运行时undefined。
仓库实证:fixture 的 use 模式在 e2e 层同样成立
e2e/studio/utils/test.ts 用 Playwright 的 base.extend 演示了同一约定:page fixture 通过 addInitScript 预置 localStorage 后调用 await use(page),use 返回后执行清理。虽然 e2e 使用 Playwright 而非 Vitest,但“setup → use() → teardown”的三段式结构与 Vitest fixture 完全一致,印证了这一模式在仓库测试体系中的一致性。
八、钩子执行顺序与 sequence.hooks 配置
Vitest 默认按 stack(栈) 顺序执行钩子:
beforeAll(按注册顺序)beforeEach(按注册顺序)- 测试本体
afterEach(逆序执行)afterAll(逆序执行)
即 setup 正序、teardown 逆序,如同“穿衣服 / 脱衣服”的栈式配对。通过 sequence.hooks 可切换策略:
defineConfig({
test: {
sequence: {
hooks: 'list', // 'stack' (default), 'list', 'parallel'
},
},
})
三个取值的适用场景:
'stack'(默认):setup 正序 / teardown 逆序,适合有依赖层次的资源(先启动服务再注入配置,则先撤配置再停服务);'list':before 与 after 均按注册顺序执行,适合 teardown 顺序与 setup 无关的场景;'parallel':同组钩子并行执行,最大化 setup/teardown 吞吐,要求钩子间无顺序依赖。
仓库中的 Vitest 配置文件(如 packages/ui/vitest.config.ts、apps/studio/vitest.config.ts)均未覆盖 sequence.hooks,即沿用默认 stack 语义——阅读仓库既有测试的钩子时序时可按此默认假设分析。
九、关键要点速查
结合文档 Key Points 与仓库实践,收束为可核查清单:
- 类型检查阶段不执行钩子:hook 代码不参与
vitest typecheck的执行,钩子里的逻辑不能靠类型测试覆盖; - 能用返回清理函数就不要重复写 after*:setup 与 cleanup 就近声明,降低遗漏风险(仓库对应实例:withSetupCleanup、withTestDatabase);
aroundEach/aroundAll必须调用runTest()/runSuite():不显式调用则测试 / suite 不会运行;onTestFinished无论成败都执行:资源释放的唯一安全挂载点;失败诊断用onTestFailed;- 并发测试必须使用上下文钩子:
test.concurrent内通过({ onTestFinished })解构注册清理; - 时序存疑时先确认
sequence.hooks:默认 stack 语义下,多个 before 正序、多个 after 逆序。
十、适用前提与边界说明
- 本文所述钩子 API 基于 Vitest 3.x,与仓库 SKILL.md 声明的“based on Vitest 3.x, generated at 2026-01-28”一致;各子包通过 pnpm
catalog:引用 vitest(如 apps/studio/package.json),版本由 monorepo 根目录统一约束; - 钩子超时、
sequence.hooks等配置项的作用范围以各子包vitest.config.ts为准,apps/studio额外启用了globals: true(全局注入钩子,可省略 import)与 CI 条件重试,阅读该目录测试文件时需将这两点纳入时序分析; e2e/studio目录使用 Playwright 而非 Vitest,文中引用其代码仅为印证 fixture 三段式约定,其钩子 API 细节不在本文文档范围内。
参考起点:.agents/skills/vitest/references/core-hooks.md;同目录下的 core-describe.md(suite 组织)与 features-context.md(fixture 与 context)可与本文配合阅读,构成 Vitest 测试生命周期的完整知识链。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0625
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00