news 2026/9/13 11:36:34

Vitest TestSpecification 深入解析:用高级 Node.js API 精确调度测试模块的运行

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Vitest TestSpecification 深入解析:用高级 Node.js API 精确调度测试模块的运行

Vitest TestSpecification 深入解析:用高级 Node.js API 精确调度测试模块的运行

【免费下载链接】vitestNext generation testing framework powered by Vite.项目地址: https://gitcode.com/GitHub_Trending/vi/vitest

TestSpecification是 Vitest 高级 Node.js API 的核心数据结构,用于描述"要运行哪个测试模块以及如何运行它"(模块路径 + 运行参数)。本文以 docs/api/advanced/test-specification.md 为骨架,结合仓库源码 packages/vitest/src/node/test-specification.ts 及TestProjectVitest实例的调用链,完整讲解它的每个属性、过滤参数与序列化机制。读完你将掌握:如何通过project.createSpecification构造规范、用testLines/testNamePattern/testIds/testTagsFilter四种维度精确圈定要执行的测试,并将其交给vitest.runTestSpecifications驱动一次受控的测试运行,为自定义 CLI、IDE 插件或测试编排工具提供底层能力。

概述:TestSpecification 是什么

TestSpecification描述了一个"作为测试运行的模块及其参数"。它把"运行哪个文件"与"只运行文件中的哪些测试"这两件事封装成一个可传递、可序列化、可缓存的对象,是 Vitest 在编程式 API(Programmatic API)中调度测试的最小单元。

const specification = project.createSpecification( resolve('./example.test.ts'), { testLines: [20, 40], testNamePattern: /hello world/, testIds: ['1223128da3_0_0_0', '1223128da3_0_0'], testTagsFilter: ['frontend and backend'], } // optional test filters )

它只能通过TestProject.createSpecification方法创建,不能直接new(构造函数在源码中标记为@internal)。从源码实现看,createSpecification期望传入已解析(resolved)的模块标识符,它不会自动解析文件路径,也不会检查文件在文件系统上是否存在:

createSpecificationexpects resolved module identifier. It doesn't auto-resolve the file or check that it exists on the file system.

源码 packages/vitest/src/node/test-specification.ts#L53-L79 中的构造函数明确展示了TestSpecification的完整构成:

constructor( project: TestProject, moduleId: string, pool: Pool, testLinesOrOptions?: number[] | TestSpecificationOptions | undefined, taskIdOverride?: string, ) { const projectName = project.config.name this.taskId = taskIdOverride ?? generateFileHash( relative(project.config.root, moduleId), projectName, { typecheck: pool === 'typescript', __vitest_label__: project.config.mergeReportsLabel }, ) this.project = project this.moduleId = moduleId this.pool = pool if (Array.isArray(testLinesOrOptions)) { this.testLines = testLinesOrOptions } else if (testLinesOrOptions && typeof testLinesOrOptions === 'object') { this.testLines = testLinesOrOptions.testLines this.testNamePattern = testLinesOrOptions.testNamePattern this.testIds = testLinesOrOptions.testIds this.testTagsFilter = testLinesOrOptions.testTagsFilter } }

从这个实现可以得出几个关键事实:

  • 第二个参数有两种形态:既可以传number[](等价于只设置testLines),也可以传一个TestSpecificationOptions对象(包含testLinestestNamePatterntestIdstestTagsFilter四个可选字段)。文档中的示例正是第二种形态。
  • taskId由构造器自动生成:基于"模块相对路径 + 项目名"通过generateFileHash计算得出(typecheck池或mergeReports场景会附加额外标记),这正是 TestSuite/TestCase 的id体系里那一段"file hash"的来源。
  • project.createSpecification每次都会返回一个新实例,不会被自动缓存(详见 TestProject 文档 的警告)。这一点与vitest.getModuleSpecifications会返回缓存规范的机制形成对比。

TestSpecificationOptions的接口定义在 packages/vitest/src/node/test-specification.ts#L8-L13:

export interface TestSpecificationOptions { testNamePattern?: RegExp testIds?: string[] testLines?: number[] testTagsFilter?: string[] }

属性逐一解析

TestSpecification暴露 7 个核心只读属性(taskIdprojectmoduleIdpooltestModuletestLinestestNamePatterntestIdstestTagsFilter)与一个toJSON()方法。下面逐一展开。

taskId

public readonly taskId: string

对应测试模块的标识符。它是确定性的:同一项目、同一模块、同一顺序下的同一测试,多次运行得到的taskId不变。taskIdgenerateFileHash基于模块相对路径与项目名计算,因此它是TestSpecification与运行时任务(Task)之间的桥梁——testModule属性正是靠它从全局状态中反查模块实例的(见下文)。

该 ID 可能以负号开头(如-1223128da3_0_0_0),官方文档明确告诫:不要尝试解析这个 ID 的结构

project

public readonly project: TestProject

指向该规范所属的TestProject。由于一个规范总是从某个具体的project.createSpecification()诞生,它天然地把"测试文件"与"项目上下文(config、pool、provide 上下文等)"绑定在一起。这也是规范可以在多项目(projects)场景下精确路由的根本原因。

moduleId

public readonly moduleId: string

模块在 Vite 模块图(Module Graph)中的 ID。通常是一个使用posix 分隔符的绝对文件路径(即使是在 Windows 上):

'C:/Users/Documents/project/example.test.ts' // ✅ '/Users/mac/project/example.test.ts' // ✅ 'C:\\Users\\Documents\\project\\example.test.ts' // ❌

注意最后一行使用反斜杠是错误写法。该值与 TestModule.moduleId 一致,对应 Vite 的ModuleGraphid;对于不在磁盘上的文件,它也可以是虚拟 id。createSpecification不负责解析或校验该路径,传入前应自行用resolve(文档示例使用node:path/posixresolve)完成绝对化。

testModule

get testModule(): TestModule | undefined

与规范关联的TestModule实例;如果测试尚未排队执行,该值为undefined。源码 packages/vitest/src/node/test-specification.ts#L84-L90 展示了它的取回机制:

get testModule(): TestModule | undefined { const task = this.project.vitest.state.idMap.get(this.taskId) if (!task) { return undefined } return this.project.vitest.state.getReportedEntity(task) as TestModule | undefined }

即:用taskId从全局状态state.idMap中查找内部任务,再通过getReportedEntity转换为面向用户的TestModule。这也印证了taskId的"索引键"角色——规范与运行结果通过它关联。

pool

public readonly pool: Pool

测试模块将要运行的池(pool)(如'threads''forks''typescript''browser'等)。pool由构造器第三个参数决定,公开的createSpecification通常会自动推断。

::: danger 多池警告 在一个测试项目中,如果启用了typecheck.enabled,同一个moduleId可能同时存在普通测试池与 typecheck 池两种运行形态,从而出现多个moduleId相同但pool不同的规范。在后续版本中,Vitest 项目将只支持单一 pool,这种多池情形会被移除。 :::

这一警告在 Vitest 实例文档 中有呼应:Vitest 3 起,若poolMatchGlob配置了多个池或启用了 typecheck,就可能出现多个 moduleId 相同的规范。因此官方反复强调:不要依赖规范对象引用的同一性createSpecification每次返回新实例、缓存基于moduleId + pool)。

testLines

public testLines: number[] | undefined

测试在源码中定义位置的行号数组。只有当createSpecification收到的是数组(或 options 中的testLines字段)时才会被设置。它对应测试在文件中的 location 行号,Vitest 只会运行这些行上定义的测试。

重要约束:如果这些行中至少有一行没有测试,整个 suite 会运行失败。文档给出了一对正确的示例:

::: code-group

const specification = project.createSpecification( resolve('./example.test.ts'), [3, 8, 9], )
import { test, describe } from 'vitest' test('verification works') describe('a group of tests', () => { // [!code error] // ... test('nested test') test.skip('skipped test') })

:::

这里[3, 8, 9]指向第 3 行(顶层test('verification works'))以及第 8、9 行(describe内的两个test)。注意文档把第 69 行标记为[!code error]——即describe声明行(示例中为第 6 行,代码高亮显示为第 3 行注释旁)——但真正被选中的是 3/8/9 这三行测试定义行。

从调用链看,testLines最终会通过 packages/vitest/src/node/pool.ts#L152-L167 被写入运行时上下文:

taskGroup.push({ context: { files: specs.map(spec => ({ filepath: spec.moduleId, fileTags: tags.get(spec), testLocations: spec.testLines, testNamePattern: spec.testNamePattern, testIds: spec.testIds, testTagsFilter: spec.testTagsFilter, })), // ... }, })

也就是说,规范的四个过滤维度(testLinestestLocationstestNamePatterntestIdstestTagsFilter)会被原样传给 worker 运行时,由运行端在收集/执行阶段进行过滤。

另外,CLI 的file:line位置过滤(如basic/foo.js:10)最终也是通过specifications.ts把行号分组后调用project.createSpecification(file, lines)实现的(见 packages/vitest/src/node/specifications.ts#L55-L88),并且在找不到对应文件时抛出LocationFilterFileNotFoundError

testNamePattern 4.1.0

public testNamePattern: RegExp | undefined

匹配本模块内测试名称的正则表达式。如果设置了该值,它会覆盖全局的testNamePattern配置(若全局配置存在的话)。该特性自 Vitest 4.1.0 起可用。

它对应编程式 API 中的vitest.setGlobalTestNamePattern的"局部化"版本:全局 pattern 影响所有测试,而规范上的 pattern 只影响当前模块内的测试选择。

testIds 4.1.0

public testIds: string[] | undefined

本规范内要运行的任务 ID 数组。任务 ID 即上文提到的确定性 ID(如1223128da3_0_0),你可以先通过收集阶段拿到具体测试的id,再精确指定"只运行这几个测试"。自 Vitest 4.1.0 起可用。

testTagsFilter 4.1.0

public testTagsFilter: string[] | undefined

测试必须通过的标签过滤器,未通过标签匹配的测试不会进入运行。多个过滤器按AND处理——测试必须同时满足所有标签条件才会被包含。该特性自 Vitest 4.1.0 起可用,与 TestCase.tags(4.1.0 起暴露的显式/隐式标签)配合使用。

toJSON:把规范序列化给浏览器模式与 UI

function toJSON(): SerializedTestSpecification

toJSON生成一个 JSON 友好的对象,可被 Browser Mode 或 Vitest UI 消费。从源码 packages/vitest/src/node/test-specification.ts#L92-L107 看,序列化结果为三元组数组:

toJSON(): SerializedTestSpecification { return [ { name: this.project.config.name, root: this.project.config.root, }, this.moduleId, { pool: this.pool, testLines: this.testLines, testIds: this.testIds, testNamePattern: this.testNamePattern, testTagsFilter: this.testTagsFilter, }, ] }

即:(项目名与根目录,模块 ID,过滤参数对象)。由于它是纯数据,可以安全地跨进程传递(例如主线程 → 浏览器运行端 → UI 展示),也正因如此,toJSON成为 Vitest UI 中"重跑指定测试"、浏览器模式中任务下发的基础格式。

实战:从规范到一次完整的受控运行

把上面所有知识点串起来,一个完整的最小工作流如下(同样适用于 docs/api/advanced/test-project.md 中的createSpecification示例):

import { createVitest } from 'vitest/node' import { resolve } from 'node:path/posix' const vitest = await createVitest('test', { watch: false }) // 1. 拿到目标项目(多项目场景下可按名称查找) const project = vitest.projects[0] // 2. 构造规范:文件 + 四种可选过滤维度 const specification = project.createSpecification( resolve('./example.test.ts'), { testLines: [20, 40], testNamePattern: /hello world/, testIds: ['1223128da3_0_0_0', '1223128da3_0_0'], testTagsFilter: ['frontend', 'backend'], // 多个过滤器按 AND 处理 }, ) // 3. 运行 await vitest.runTestSpecifications([specification]) // 4. 收尾 await vitest.close()

围绕这一流程,有几个来自 Vitest 实例 API 的配套能力值得记住:

  • 批量获取规范vitest.globTestSpecifications(filters)会通过每个项目的project.globTestFiles收集全部测试并构造规范(带缓存);vitest.getModuleSpecifications(moduleId)返回与某模块 ID 相关的规范(缓存基于moduleId + pool);vitest.clearSpecificationsCache(moduleId?)可清理缓存。
  • 运行入口runTestSpecifications(specifications, allTestsRun = false)按规范运行测试(第二个参数供覆盖率提供者判断是否需要纳入未覆盖文件);rerunTestSpecifications额外触发onWatcherRerun/onTestsRerun等事件,适合 watch 场景。注意runTestSpecifications不会触发这些回调。
  • 反向生成TestModule.toTestSpecification()(4.1.0)、TestSuite.toTestSpecification()TestCase.toTestSpecification()可以从已收集的任务反推出新的规范,用于"只重跑这个模块/套件/用例"。
  • 收集而不执行vitest.experimental_parseSpecification(spec)会用 Rollup 的parseAst对文件做静态分析、收集其中的测试而不运行vitest.parseSpecifications(specs)(5.0.0)则批量收集一批规范,默认并发数不超过os.availableParallelism()。动态命名(模板字符串、for/each展开)的测试会被注入dynamic: true标记且无法用于过滤。
  • 未运行时的表现:若规范对应的测试还未排队执行,testModuleundefined,TestModule.state() 会返回额外的queued状态。

关键注意事项汇总

  1. moduleId必须是已解析的绝对路径(posix 分隔符),createSpecification不做自动解析与存在性检查;传错路径可能导致任务 ID 与预期不符或运行期找不到文件。
  2. 每次createSpecification都产生新实例,不会被自动缓存;不要用===判断规范是否相同,也不要依赖规范引用的持久性。
  3. testLines中的行必须真的有测试,否则整个 suite 失败;行号对应测试在源码中的定义位置(location)。
  4. testNamePattern会覆盖全局配置;它、testIdstestTagsFilter均为 4.1.0 新增能力,使用时请确认 Vitest 版本。
  5. 多池共存:启用typecheck.enabled时同一文件可能有多个规范(不同pool),后续版本将收敛为单池,请勿依赖该多池行为编写长期代码。
  6. taskId不要手工解析;它可能以-开头,只应作为不透明标识符使用。
  7. toJSON()的输出是跨进程数据(Browser Mode / Vitest UI 消费),不要依赖其中的内部字段顺序以外的语义。

TestSpecification把"文件 + 项目 + 池 + 过滤条件"四个维度收敛为一个不可变的核心描述对象,是 Vitest 编程式 API 中连接配置(createVitest)、项目(TestProject)、任务(TestModule/TestSuite/TestCase)与执行(runTestSpecifications)的枢纽。需要继续深入时,可以直接阅读 packages/vitest/src/node/test-specification.ts 的完整实现,以及它被消费的两端:specifications.ts(规范的生产与缓存)与 pool.ts(规范向运行时上下文的传递)。

【免费下载链接】vitestNext generation testing framework powered by Vite.项目地址: https://gitcode.com/GitHub_Trending/vi/vitest

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 11:31:24

刚挠结合PCB询价资料技术解析与避坑指南

1. 刚挠结合 PCB 询价资料全解析:这不是一份报价单,而是一张技术通关地图刚挠结合 PCB——这个词在电子制造圈里,既让人眼前一亮,又下意识皱眉。它不是普通PCB的简单升级,而是把“刚性板的稳定”和“柔性板的弯折”硬生…

作者头像 李华
网站建设 2026/9/13 11:29:17

基于BERT与Django的服装评论智能分析系统设计与实现

1. 项目概述与核心价值这个毕业设计项目构建了一个面向服装领域的用户评论智能分析系统,融合了大数据处理与深度学习技术。系统能够自动解析电商平台海量服装评论中的情感倾向、产品特征和用户关注点,为商家提供产品改进、营销策略优化的数据支持。为什么…

作者头像 李华