Vitest describe API 深度解析:从测试分组到参数化与并发套件 —— 以 Supabase 单仓实践为例
本文围绕 Vitest 的 describe(套件)API 展开:如何用它把相关测试组织成逻辑清晰的套件树、如何借助 options 与修饰符(skip/only/concurrent/shuffle 等)控制套件的执行行为、以及 describe.each / describe.for 参数化套件的写法。读完后,你将掌握 Vitest 套件组织的完整 API 面,并能在 Supabase 这类 pnpm monorepo 中直接定位真实用例(如 Studio 前端的参数化测试),把分组、共享 setup 与条件跳过落地到日常测试代码中。
describe 的定位:测试分组的入口
Supabase 仓库在 .agents/skills/vitest/SKILL.md 中把 Vitest 的 API 参考分成 Core / Features / Advanced 三层,其中 Describe API 被列为 Core 六项之一:“describe/suite for grouping tests and nested suites”,对应参考文件即 core-describe.md。该参考是基于 Vitest 3.x 于 2026-01-28 生成的(见 GENERATION.md),而本仓在 pnpm-workspace.yaml 中通过 catalog 将 vitest 统一钉在 ^4.1.4——describe 这套 API 在 3.x 与 4.x 间保持兼容,参考内容可直接用于本仓的测试代码。
describe 的核心职责是把相关测试归入一个“套件(suite)”,用于组织代码与共享 setup。最基本的用法:
import { describe, expect, test } from 'vitest'
describe('Math', () => {
test('adds numbers', () => {
expect(1 + 1).toBe(2)
})
test('subtracts numbers', () => {
expect(3 - 1).toBe(2)
})
})
// 别名:suite
import { suite } from 'vitest'
suite('equivalent to describe', () => {})
要点:
- 顶层的
test并不强制包在describe里——它们属于一个隐式的“文件套件”(file suite); suite是describe的完全等价别名,可按团队命名偏好选择。
嵌套套件:用套件树表达测试结构
当行为随状态变化时(例如“登录/未登录”),嵌套套件比长字符串的测试名更清晰:
describe('User', () => {
describe('when logged in', () => {
test('shows dashboard', () => {})
test('can update profile', () => {})
})
describe('when logged out', () => {
test('shows login page', () => {})
})
})
嵌套的语义有两点需要记住:
- 选项继承:子套件会继承父套件的 options(
timeout、retry等); - Hooks 作用域:
beforeAll/afterEach等钩子只对其所在套件及嵌套子套件生效,跨分支互不干扰。
套件级 options:一次设置,全组生效
把配置写在套件上,组内所有测试自动继承,避免逐条重复:
// 组内所有测试继承该超时
describe('slow tests', { timeout: 30_000 }, () => {
test('test 1', () => {}) // 30s 超时
test('test 2', () => {}) // 30s 超时
})
timeout 只是可传入 options 的代表之一,retry 等执行控制项同样适用,且会被嵌套子套件继续继承。对于本仓这类涉及网络请求的组件测试(Studio 中有大量 mock API 的 .test.tsx),把超时收敛到套件级别是控制 CI 抖动成本的常用手段。
套件修饰符:控制“跑不跑、怎么跑”
Vitest 在 describe 上提供了与 test 对齐的一组修饰符,覆盖跳过、聚焦、占位、并发与乱序五类执行控制。
跳过套件:skip 与条件跳过
describe.skip('skipped suite', () => {
test('wont run', () => {})
})
// 条件跳过 / 条件运行
describe.skipIf(process.env.CI)('not in CI', () => {})
describe.runIf(!process.env.CI)('only local', () => {})
skipIf / runIf 接收一个布尔表达式(或同步函数),在运行时决定整个套件是否参与执行。典型场景是把只在本地 mock server 可用时才成立的集成用例从 CI 中剔除,同时保留代码不被注释废弃。
聚焦套件:only
describe.only('only this suite runs', () => {
test('runs', () => {})
})
存在 only 时,只运行被聚焦的测试/套件。注意这是调试工具,提交前应移除,否则会悄悄缩小测试覆盖面。
占位套件:todo
describe.todo('implement later')
用于登记尚未实现的测试区域,让套件树对“欠账”可见。
并发套件:concurrent 与嵌套 sequential
// 组内测试并行执行
describe.concurrent('parallel tests', () => {
test('test 1', async ({ expect }) => {})
test('test 2', async ({ expect }) => {})
})
并发套件的注意事项:
- 测试间必须相互独立,不共享可变状态;
- 使用上下文解构出来的
expect(即async ({ expect }))来做快照断言,保证快照归属到具体测试; - 组内如确有依赖顺序的步骤,可以嵌套
describe.sequential把这一段“串行化”:
describe.concurrent('parallel', () => {
test('concurrent 1', async () => {})
describe.sequential('must be sequential', () => {
test('step 1', async () => {})
test('step 2', async () => {})
})
})
乱序套件:shuffle
describe.shuffle('random order', () => {
test('test 1', () => {})
test('test 2', () => {})
test('test 3', () => {})
})
// 等价写法:通过 options
describe('random', { shuffle: true }, () => {})
随机顺序能尽早暴露“测试之间有隐式顺序依赖”的问题。乱序基于 sequence.seed 配置生成,因此同一 seed 下结果可复现,排查失败时可以固定种子重现同一顺序。
参数化套件:describe.each 与 describe.for
当同一组断言要在多组输入上重复时,参数化套件比复制粘贴更合适。Vitest 提供两种写法:
describe.each:对象数组 + 模板串
describe.each([
{ name: 'Chrome', version: 100 },
{ name: 'Firefox', version: 90 },
])('$name browser', ({ name, version }) => {
test('has version', () => {
expect(version).toBeGreaterThan(0)
})
})
模板串中的 $name 会被替换为每组数据里对应字段的值,生成如 “Chrome browser”“Firefox browser” 的子套件名。
describe.for:元组数组 + printf 风格占位符
describe.for([
['Chrome', 100],
['Firefox', 90],
])('%s browser', ([name, version]) => {
test('has version', () => {
expect(version).toBeGreaterThan(0)
})
})
%s 依次对应元组中的位置参数,适合参数少、无需字面命名的场景。
本仓真实用例:Studio 的参数化测试
Supabase 单仓中已在使用 describe.each。例如 Studio 的计费面板组件测试 PlanUpdateSidePanel.test.tsx:
describe.each(['fullscreen', 'fullscreen-gaps'])('%s variant', (flag) => {
// 对每个布局变体重复同一组断言
})
同一文件中(第 359 行)还有一处 describe.each(['control', 'parity', 'gaps']) 校验不同变体保持 sheet 外壳结构一致;而 logs-query.test.tsx 则用 describe.each(['free', 'pro', 'team', 'enterprise'])('upgrade modal for %s', ...) 把升级弹窗的断言铺到四个订阅档位上。这些用例的共同模式是:用扁平的字符串数组 + %s 模板名,把“同一逻辑 × N 个输入”压缩成一个可维护的套件——这正是参数化套件在本仓中的典型用法。
套件内的 Hooks:共享 setup 的边界
describe 与 hooks 配合,是最常见的共享资源模式。以下示例展示“套件级创建、测试级清理”的完整生命周期:
describe('Database', () => {
let db
beforeAll(async () => {
db = await createDb()
})
afterAll(async () => {
await db.close()
})
beforeEach(async () => {
await db.clear()
})
test('insert works', async () => {
await db.insert({ name: 'test' })
expect(await db.count()).toBe(1)
})
})
钩子语义与套件作用域强绑定:
beforeAll/afterAll在套件首/末各执行一次,适合昂贵资源的创建与销毁;beforeEach/afterEach在每个测试前后执行,适合状态重置;- 钩子只对当前套件及其嵌套子套件生效——这正是“嵌套时各分支独立 setup”的结构保证。
修饰符可自由链式组合
所有修饰符可任意叠加,顺序不影响语义:
describe.skip.concurrent('skipped concurrent', () => {})
describe.only.shuffle('only and shuffled', () => {})
describe.concurrent.skip('equivalent', () => {})
即“跳过 + 并发”“聚焦 + 乱序”等组合都能表达。实践中建议把组合控制在两类修饰符以内,避免意图含糊。
关键要点速查与在 monorepo 中运行
把整份参考收敛为五条可直接执行的结论:
- 顶层测试属于隐式的文件套件,不必强行包一层
describe; - 嵌套套件继承父级 options(
timeout、retry等),子级可覆盖; - Hooks 的作用域严格限定在其所在套件及嵌套子套件内;
describe.concurrent下做快照断言时,使用上下文里的expect;shuffle的随机顺序由sequence.seed配置决定,可复现。
在 Supabase 这个 pnpm workspace 里,vitest 版本由 pnpm-workspace.yaml 的 catalog 统一钉在 ^4.1.4,各包的 package.json 以 "vitest": "catalog:" 引用(如 packages/ui/package.json 的 "test": "vitest" 脚本、apps/studio/package.json 的依赖声明)。因此查看本文所述 API 的落地形态,最快的路径是:
- 读参考文件 core-describe.md;
- 对照同目录下的 core-test-api.md 与 core-hooks.md 补齐
test与 hooks 的对应修饰符; - 到
apps/studio与packages/ui等包的*.test.tsx中检索describe.each等模式,验证参数化套件在真实业务测试中的写法。
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 StartedRust0627
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