首页
/ Vitest describe API 深度解析:从测试分组到参数化与并发套件 —— 以 Supabase 单仓实践为例

Vitest describe API 深度解析:从测试分组到参数化与并发套件 —— 以 Supabase 单仓实践为例

2026-09-06 11:57:12作者:卓艾滢Kingsley

本文围绕 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);
  • suitedescribe 的完全等价别名,可按团队命名偏好选择。

嵌套套件:用套件树表达测试结构

当行为随状态变化时(例如“登录/未登录”),嵌套套件比长字符串的测试名更清晰:

describe('User', () => {
  describe('when logged in', () => {
    test('shows dashboard', () => {})
    test('can update profile', () => {})
  })

  describe('when logged out', () => {
    test('shows login page', () => {})
  })
})

嵌套的语义有两点需要记住:

  1. 选项继承:子套件会继承父套件的 options(timeoutretry 等);
  2. 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.eachdescribe.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 中运行

把整份参考收敛为五条可直接执行的结论:

  1. 顶层测试属于隐式的文件套件,不必强行包一层 describe
  2. 嵌套套件继承父级 options(timeoutretry 等),子级可覆盖;
  3. Hooks 的作用域严格限定在其所在套件及嵌套子套件内;
  4. describe.concurrent 下做快照断言时,使用上下文里的 expect
  5. 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 的落地形态,最快的路径是:

  1. 读参考文件 core-describe.md
  2. 对照同目录下的 core-test-api.mdcore-hooks.md 补齐 test 与 hooks 的对应修饰符;
  3. apps/studiopackages/ui 等包的 *.test.tsx 中检索 describe.each 等模式,验证参数化套件在真实业务测试中的写法。
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388