拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Vitest describe 完全指南:Suite 分组、选项继承与源码级原理剖析

Vitest describe 完全指南Suite 分组、选项继承与源码级原理剖析【免费下载链接】vitestNext generation testing framework powered by Vite.项目地址: https://gitcode.com/GitHub_Trending/vi/vitestdescribe别名suite是 Vitest 中用于把相关测试与基准benchmark组织成测试套件Suite的核心 API。本文将基于 Vitest 官方文档与仓库源码系统讲解describe的两种函数签名、套件的嵌套与隐式套件机制、测试选项的继承规则以及skip/only/concurrent/shuffle/todo/each/for等全部链式变体并深入packages/vitest/src/runtime/runner/suite.ts等源码揭示其底层实现帮助你写出结构清晰、可维护、可精细控制执行行为的测试代码。一、为什么需要describe从散乱测试到结构化套件在 Vitest 中写在文件顶层top level的test会被自动收集到该文件对应的**隐式套件implicit suite**中。而describe允许你在当前上下文中显式定义一个新的套件将一组相关的测试、基准以及嵌套的更深层套件组织起来。使用套件带来的核心收益有三个逻辑分组把针对同一模块、同一功能或同一场景的测试聚拢在一起测试输出更易读失败时能快速定位到具体模块共享生命周期套件级别可以通过 lifecycle hooksbeforeAll/beforeEach/afterEach/afterAll等统一设置与清理避免在每个测试里重复代码选项批量下发套件级选项timeout、retry、concurrent、shuffle 等会继承给套件内所有测试与嵌套套件实现一处配置处处生效。一个最基础的describe使用示例来自官方文档的basic.spec.tsimport { describe, expect, test } from vitest const person { isActive: true, age: 32, } describe(person, () { test(person is defined, () { expect(person).toBeDefined() }) test(is active, () { expect(person.isActive).toBeTruthy() }) test(age limit, () { expect(person.age).toBeLessThanOrEqual(32) }) })二、函数签名两种调用形式describe具有两种等价的重载签名function describe( name: string | Function, body?: () unknown, timeout?: number ): void function describe( name: string | Function, options: SuiteOptions, body?: () unknown, ): void第一种形式把**超时时间毫秒**作为第三个参数传入第二种形式把SuiteOptions对象作为第二个参数传入body函数放到第三个参数。需要注意这两种形式不可混用一旦选择了对象形式就不要再追加第四个超时参数。从源码 suite.ts 中的parseArguments可以看到Vitest 3 之后已移除了test(name, fn, { ... })这种旧签名若以对象作为第三个参数会直接抛出TypeError提示将 options 作为第二个参数提供。嵌套套件测试分层当测试存在层级关系时你可以无限嵌套describe。下面的例子把numberToCurrency的合法输入与非法输入分成两个子套件import { describe, expect, test } from vitest function numberToCurrency(value: number | string) { if (typeof value ! number) { throw new TypeError(Value must be a number) } return value.toFixed(2).toString().replace(/\B(?(\d{3})(?!\d))/g, ,) } describe(numberToCurrency, () { describe(given an invalid number, () { test(composed of non-numbers to throw error, () { expect(() numberToCurrency(abc)).toThrow() }) }) describe(given a valid number, () { test(returns the correct currency format, () { expect(numberToCurrency(10000)).toBe(10,000.00) }) }) })从源码看createSuitesuite.ts会读取collectorContext.currentSuite来确定当前套件上下文并把currentSuite?.options中的父级选项通过展开合并到新套件上const { meta: parentMeta, ...parentOptions } currentSuite?.options || {} // inherit options from current suite options { ...parentOptions, ...options, }这正是嵌套套件继承父级配置这一行为的实现基础我们将在下一节详细展开。三、Test Options套件级配置与继承你可以使用 test optionstimeout、retry、repeats、concurrent、skip、only、todo、fails、tags、meta等为套件内的每一个测试统一配置包括嵌套的更深层套件。典型的应用场景是给一组慢测试统一放大超时import { describe, test } from vitest describe(slow tests, { timeout: 10_000 }, () { test(test 1, () { /* ... */ }) test(test 2, () { /* ... */ }) // nested suites also inherit the timeout describe(nested, () { test(test 3, () { /* ... */ }) }) })这里的test 3虽然位于嵌套套件中同样继承10_000毫秒的超时这正是上文源码中父级选项展开合并的结果。shuffle选项Type:booleanDefault:false由sequence.shuffle配置Alias:describe.shuffle让套件内的测试以随机顺序执行且该选项会被嵌套套件继承import { describe, test } from vitest describe(randomized tests, { shuffle: true }, () { test(test 1, () { /* ... */ }) test(test 2, () { /* ... */ }) test(test 3, () { /* ... */ }) })从源码看shuffle 的取值优先级为链式调用.shuffle→ 本套件options.shuffle→ 父级套件options.shuffle→ 全局配置runner.config.sequence.shufflesuite.ts这正是全局默认 局部覆盖三级继承链的体现。关于meta的合并规则若在套件上配置meta自定义元数据Vitest 4.1 可用会暴露给 reporter需要注意 Vitest 只做顶层属性的浅合并不做嵌套对象的深合并——套件继承了父级meta后若测试在nested对象上覆盖部分字段整个nested对象会被整体替换同层其他字段会丢失。具体示例与警告可参考 test 文档的 meta 小节。四、describe.skip跳过整个套件Alias:suite.skip当你暂时不想运行某个describe块例如功能尚未完成、环境不满足时使用describe.skip跳过整个套件import { assert, describe, test } from vitest describe.skip(skipped suite, () { test(sqrt, () { // Suite skipped, no error assert.equal(Math.sqrt(4), 3) }) })即使套件内的断言本身是错误的被跳过也不会报错。五、describe.skipIf 与 describe.runIf按条件启停套件describe.skipIfAlias:suite.skipIf当套件与运行环境强相关、需要在不同环境下多次运行同一套测试时与其用if包裹整个套件不如用describe.skipIf在条件为真时跳过import { describe, test } from vitest const isDev process.env.NODE_ENV development describe.skipIf(isDev)(prod only test suite, () { // this test suite only runs in production })describe.runIfAlias:suite.runIfdescribe.runIf是skipIf的反向操作——条件为真时才运行import { assert, describe, test } from vitest const isDev process.env.NODE_ENV development describe.runIf(isDev)(dev only test suite, () { // this test suite only runs in development })从源码看两者的实现非常直白suite.tssuiteFn.skipIf (condition: any) (condition ? suite.skip : suite) as SuiteAPI suiteFn.runIf (condition: any) (condition ? suite : suite.skip) as SuiteAPI即skipIf(true)等价于调用suite.skiprunIf(true)等价于直接调用suite本质上是条件与链式修饰符的语法糖。六、describe.only只运行指定套件Alias:suite.only调试时使用describe.only可以让只有标记了only的套件同一文件或同一测试运行内运行其余套件全部跳过import { assert, describe, test } from vitest // Only this suite (and others marked with only) are run describe.only(suite, () { test(sqrt, () { assert.equal(Math.sqrt(4), 3) }) }) describe(other suite, () { // ... will be skipped })有时我们希望只运行某个文件里的 only 测试忽略整个测试套件里其余所有测试这些测试会污染输出。做法是直接把这个文件作为命令行参数传给vitestvitest interesting.test.ts从源码看套件的运行模式由this.only ?? options.only ? only : ...这样的链式判断决定suite.tsonly会优先于skip/todo被识别。提示在 CI 环境下 Vitest 检测到任何only标记会直接报错可通过allowOnly配置关闭该行为防止only被误提交到代码库。七、describe.concurrent并行执行套件内全部任务Alias:suite.concurrentdescribe.concurrent会让套件内所有嵌套套件和测试并行运行import { describe, test } from vitest // All suites and tests within this suite will be run in parallel describe.concurrent(suite, () { test(concurrent test 1, async () { /* ... */ }) describe(concurrent suite 2, async () { test(concurrent test inner 1, async () { /* ... */ }) test(concurrent test inner 2, async () { /* ... */ }) }) test.concurrent(concurrent test 3, async () { /* ... */ }) })退出并发{ concurrent: false }如果父级套件开启了并发无论是describe.concurrent还是全局的sequence.concurrent你可以在某个子套件上显式设置concurrent: false来退出继承的并发模式describe.concurrent(suite, () { test(concurrent test, async () { /* ... */ }) describe(sequential suite, { concurrent: false }, () { test(sequential test 1, async () { /* ... */ }) test(sequential test 2, async () { /* ... */ }) }) })这与 test 的concurrent选项 行为一致套件继承父级并发但可以局部关闭。并发下的 Snapshot 注意事项运行并发测试时Snapshot 和断言必须使用来自本地 Test Context 的expect以确保正确的测试被关联检测describe.concurrent(suite, () { test(concurrent test 1, async ({ expect }) { expect(foo).toMatchSnapshot() }) test(concurrent test 2, async ({ expect }) { expect(foo).toMatchSnapshot() }) })修饰符组合.skip、.only、.todo都可以与并发套件组合使用以下所有写法均合法describe.concurrent(/* ... */) describe.skip.concurrent(/* ... */) // or describe.concurrent.skip(/* ... */) describe.only.concurrent(/* ... */) // or describe.concurrent.only(/* ... */) describe.todo.concurrent(/* ... */) // or describe.concurrent.todo(/* ... */)从源码看createSuite返回的 API 通过createChainable([concurrent, shuffle, skip, only, todo], suiteFn)suite.ts将上述修饰符与each/for/skipIf/runIf组合成可链式调用的完整 API因此修饰符顺序可以互换。八、describe.shuffle局部随机化执行顺序Alias:suite.shuffleVitest 提供了两种全局随机化手段CLI 参数--sequence.shuffle见 CLI 文档和配置项sequence.shuffle。但如果你只想让测试套件中的一部分以随机顺序运行可以用describe.shuffle标记等价于describe(suite, { shuffle: true }, ...)import { describe, test } from vitest // or describe(suite, { shuffle: true }, ...) describe.shuffle(suite, () { test(random test 1, async () { /* ... */ }) test(random test 2, async () { /* ... */ }) test(random test 3, async () { /* ... */ }) // shuffle is inherited describe(still random, () { test(random 4.1, async () { /* ... */ }) test(random 4.2, async () { /* ... */ }) }) // disable shuffle inside describe(not random, { shuffle: false }, () { test(in order 5.1, async () { /* ... */ }) test(in order 5.2, async () { /* ... */ }) }) }) // order depends on sequence.seed option in config (Date.now() by default)关键行为有三点继承嵌套的still random子套件自动继承 shuffle局部关闭通过{ shuffle: false }可在内部关闭随机化恢复顺序执行随机种子随机顺序依赖于配置中的sequence.seed默认Date.now()同一 seed 下顺序可复现便于排查顺序相关的 flaky 测试。同样.skip、.only、.todo均可与随机套件组合使用。仓库实测用例可见 test/e2e/fixtures/reported-tasks/1_first.test.ts 中describe.shuffle(shuffled group, ...)的真实用法。九、describe.todo占位待实现的套件Alias:suite.todo使用describe.todo为将来要实现的套件打占位标记测试报告中会为该套件显示一条条目方便你统计还有多少测试待实现// An entry will be shown in the report for this suite describe.todo(unimplemented suite)不需要提供回调函数报告会明确列出这条 TODO 套件。十、describe.each 与 describe.for数据驱动套件当你有多组测试依赖同一批数据时用describe.each为每组数据各生成一个套件。describe.each数组形式import { describe, expect, test } from vitest describe.each([ { a: 1, b: 1, expected: 2 }, { a: 1, b: 2, expected: 3 }, { a: 2, b: 1, expected: 3 }, ])(describe object add($a, $b), ({ a, b, expected }) { test(returns ${expected}, () { expect(a b).toBe(expected) }) test(returned value not be greater than ${expected}, () { expect(a b).not.toBeGreaterThan(expected) }) test(returned value not be less than ${expected}, () { expect(a b).not.toBeLessThan(expected) }) })套件标题中的$a、$b会分别替换为每个用例对应字段的值让报告一目了然。describe.each模板字符串形式除了数组还可以用**标签模板字符串tagged template literal**描述用例第一行是列名用|分隔之后每一行是一条数据用${value}语法书写。import { describe, expect, test } from vitest describe.each a | b | expected ${1} | ${1} | ${2} ${a} | ${b} | ${ab} ${[]} | ${b} | ${b} ${{}} | ${b} | ${[object Object]b} ${{ asd: 1 }} | ${b} | ${[object Object]b} (describe template string add($a, $b), ({ a, b, expected }) { test(returns ${expected}, () { expect(a b).toBe(expected) }) })注意当某列的值是对象/数组时会被字符串化为其toString()结果如{}变为[object Object]这也是模板形式天然的限制。describe.for数组不再自动展开::: tipdescribe.each是为兼容 Jest 而提供的Vitest 同时提供了describe.for它简化了参数类型并与test.for保持一致。 :::describe.for与describe.each的唯一区别在于数组用例的处理方式each会把数组用例展开成多个位置参数而for不展开、直接整体传入因此回调参数需要写成数组解构形式。其他非数组场景包括模板字符串用法两者完全一致。// each spreads array case describe.each([ [1, 1, 2], [1, 2, 3], [2, 1, 3], ])(add(%i, %i) - %i, (a, b, expected) { // [!code --] test(test, () { expect(a b).toBe(expected) }) }) // for doesnt spread array case describe.for([ [1, 1, 2], [1, 2, 3], [2, 1, 3], ])(add(%i, %i) - %i, ([a, b, expected]) { // [!code ] test(test, () { expect(a b).toBe(expected) }) })源码层面的实现差异从 suite.ts 的实现可以看到二者底层差异suiteFn.eachL655-L706当所有用例都是数组arrayOnlyCases时调用suite(...)时用handler(...items)把数组展开为多个参数否则按单个值handler(i)传入suiteFn.forL708-L731无论用例是否为数组统一用handler(item)原样传入整个用例再由回调自行解构模板字符串形式由formatTemplateStringL1085-L1101解析它把第一行按|拆成列名再按列数把后续参数打包成{ 列名: 值 }的对象数组。仓库中的实测用例可以进一步印证这些 API 的组合能力例如 test/e2e/test/concurrent.test.ts 中describe.for([a, b])(%s, { concurrent: true }, ...)把for与并发选项叠加使用test/e2e/fixtures/reporters/function-as-name.test.ts 则验证了describe.each支持以函数作为套件名取函数name属性作为显示名。十一、底层原理速览describe 如何被收集与运行为了更完整地理解describe这里把前面分散的源码证据汇总成一条从调用到运行的主线API 导出describe、it、suite、test均从 runtime/runner/suite.ts 导出再经 public/index.ts 暴露为vitest包的顶层 API因此import { describe } from vitest与import { suite } from vitest完全等价创建套件createSuite()生成suiteFn主体通过parseArguments解析函数/选项对象/超时数字三种参数形态通过{ ...parentOptions, ...options }继承父级选项最终调用createSuiteCollector创建收集器收集任务collectTask(collector)会把套件工厂函数推入任务收集队列在测试文件执行阶段统一展开为真实的 Suite 任务节点类型系统SuiteOptions在 runtime/runner/types.ts 中被定义为extends TestOptions并追加shuffle?: boolean这也解释了为什么所有 test 选项timeout/retry/repeats/tags/meta 等都能直接用在describe上链式能力createChainable([concurrent, shuffle, skip, only, todo], suiteFn)为suiteFn挂上全部修饰符配合手写的each/for/skipIf/runIf最终组装成完整的SuiteAPI类型同样是SuiteOptions的来源。十二、实践建议小结默认先分组即使只有一个文件也建议用describe按被测模块划分套件配合 hooks 做共享 setup/teardown可显著提升输出可读性与维护性优先对象形式传选项describe(name, { timeout, retry, concurrent }, body)比链式修饰符更适合批量下发且与test的选项 API 完全一致链式修饰符则更适合表达skipIf/runIf这类条件语义善用继承局部覆盖shuffle、concurrent、timeout 等都会向嵌套套件继承记得用{ shuffle: false }、{ concurrent: false }做精确的局部退出数据驱动用each/for多组相似数据优先考虑describe.each/describe.for用$field占位符保持报告可读数组用例的场景选for语义更清晰且与test.for保持一致并发与快照并发套件内务必通过 Test Context 注入的expect使用toMatchSnapshot否则无法正确关联到具体的并发测试别把only留在 CIdescribe.only在 CI 下默认会触发报错除非显式配置allowOnly提交前务必移除。以上内容覆盖了describe的全部官方 API 与关键实现细节你可以直接以此为参考在项目中组织测试套件也可以结合 docs/api/describe.md、docs/api/test.md 与 runtime/runner/suite.ts 继续深入阅读。【免费下载链接】vitestNext generation testing framework powered by Vite.项目地址: https://gitcode.com/GitHub_Trending/vi/vitest创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门