
Vitest Runner API 完全指南自定义测试运行器与任务收集器实战【免费下载链接】vitestNext generation testing framework powered by Vite.项目地址: https://gitcode.com/GitHub_Trending/vi/vitest本文面向需要深度定制 Vitest 行为的开发者尤其是测试库/框架的作者。你将掌握如何通过配置runner选项注入自定义测试运行器理解VitestRunner生命周期钩子的完整调用时序与Task任务数据结构并能用createTaskCollector创建带todo/each/only等链式能力的自定义测试方法。全文以 docs/api/advanced/runner.md 为骨架并结合本仓库源码types.ts、test.ts、index.ts、suite.ts逐层剖析实现原理。::: warning 这是一套高级 API。如果你只是想运行测试参考 测试指南 即可绝大多数用户并不需要它。本 API 主要面向测试库作者用于实现自定义测试语义如 BDD 之外的 DSL、自定义断言入口、特殊执行策略等。 :::一、Runner 是什么入口与配置方式Vitest 把收集测试 → 执行测试 → 汇总结果这套流程抽象为可替换的Runner运行器。你可以通过配置文件中的runner选项指向一个自定义运行器文件// vitest.config.ts import { defineConfig } from vitest/config export default defineConfig({ test: { runner: ./src/my-runner.ts, // 指向导出 default 构造函数的模块 }, })该配置项的完整定义见 docs/config/runner.md类型为VitestRunnerConstructor即new (config: SerializedConfig) VitestRunner的构造函数类型见 types.ts。底层加载链路从源码看自定义 runner 的加载与校验发生在 worker 侧getTestRunnerConstructor 通过moduleRunner.import(config.runner)加载指定模块若模块没有 default 导出或 default 不是函数会抛出Runner must export a default function, but got ...。resolveTestRunner 实例化构造函数并自动注入两个私有成员moduleRunner来自vite/module-runner的ModuleRunner实例以不可枚举属性注入若实例没有config属性会补上config若没有importFile方法会直接抛错Runner must implement importFile method.。为了不让自定义 runner 重复实现 RPC 通信resolveTestRunner还会对onTaskUpdate、onTestAnnotate、onCollectStart、onCollected、onAfterRunFiles负责收集覆盖率并上报、onAfterRunTask负责bail提前终止逻辑做方法包裹自动转发给主线程。也就是说一个最小的可用 runner 只需要实现importFile与config两个成员其余钩子均为可选。二、VitestRunner 接口全景每个生命周期钩子VitestRunner接口源码定义见 types.ts描述了一个运行器应当具备的全部能力。下面按收集阶段 / 执行阶段 / 文件级 / 工具能力四组逐一解读。收集阶段钩子签名说明onBeforeCollect(paths: string[]) unknown在真正收集与运行测试之前最先被调用onCollectStart(file: File) unknown文件任务已创建、但尚未开始收集时调用onCollected(files: File[]) unknown收集完成、进入onBeforeRunFiles之前调用onCollectStart在TestRunner中会把workerState.current指向当前文件test.tsresolveTestRunner还会在其外层包裹rpc().onQueued(file)上报排队事件onCollected则被包裹了prepareDuration/environmentLoad时长上报与retry.condition函数清理逻辑函数无法被结构化克隆需在 RPC 前剥离见 index.ts。执行阶段task 级钩子钩子签名调用时机与语义onBeforeRunTask(test: Test) unknown运行单个测试之前此时还没有resultonBeforeTryTask(test: Test, options: { retry: number; repeats: number }) unknown真正执行测试函数之前此时result已存在含state与startTimeonAfterTryTask(test: Test, options: { retry: number; repeats: number }) unknown测试函数执行完毕后立即调用尚无新状态若测试函数抛出异常则不会被调用onAfterRetryTask(test: Test, options: { retry: number; repeats: number }) unknown重试retry流程落定之后调用此时测试拥有新状态且所有after钩子均已执行onAfterRunTask(test: Test) unknown结果与状态都已写入之后调用onTaskFinished(test: Test) unknown任务运行结束、但清理钩子cleanup hooks尚未执行时调用其中TestTryOptions即{ retry: number; repeats: number }源码见 types.ts。以默认TestRunner的实现为例你可以直观看到这些钩子的真实工作onBeforeRunTask若收到取消信号cancelRun则把test.mode置为skiponBeforeTryTask执行clearModuleMocks按clearMocks/mockReset/restoreMocks/unstubEnvs/unstubGlobals配置清理vi状态、重置快照客户端并向全局 expect 注入断言计数状态onAfterTryTask校验expect.assertions(n)/expect.hasAssertions()/requireAssertions的断言数量是否达标onAfterRunTask在开启logHeapUsage时记录堆内存占用并恢复workerState.current。执行阶段suite 级钩子与运行器替换钩子签名说明onBeforeRunSuite(suite: Suite) unknown运行单个套件之前此时没有resultonAfterRunSuite(suite: Suite) unknown运行完毕已有状态与结果TestRunner的onAfterRunSuite是快照功能的核心落点test.ts它把跳过的测试标记为非废弃快照、调用snapshotClient.finish写快照、在updateSnapshot none时把存在废弃快照升级为失败错误并通过rpc().snapshotSaved(result)上报。这也是文档强调快照支持依赖 runner、建议继承TestRunner的原因。更进一步接口允许你整体替换默认执行逻辑runSuite?: (suite: Suite) Promisevoid若定义将取代 Vitest 默认的套件分区与处理流程before/after钩子不会被忽略。runTask?: (test: TaskPopulated) Promisevoid若定义将取代默认的测试执行逻辑适合为自定义测试函数提供执行入口同样不会忽略before/after钩子。文件级与工具能力成员签名说明onBeforeRunFiles(files: File[]) unknown运行所有已收集文件之前onAfterRunFiles(files: File[]) unknown运行完所有文件之后onTaskUpdate(task: TaskResultPack[], events: TaskEventPack[]) Promisevoid任务状态更新时回调与 reporter 的onTaskUpdate等价但运行在与测试相同的线程中extendTaskContext(context: TestContext) TestContext测试上下文创建时回调可注入自定义属性若只想扩展上下文文档建议优先用setupFiles里的beforeAllimportFile(filepath: string, source: VitestRunnerImportSource) unknown必填。文件被导入时调用发生在两种场景收集测试、导入 setup 文件source取值为collect | setupinjectValue(key: string) unknown当test.extend使用{ injected: true }时取值会走此函数configSerializedConfig必填。公开可用的序列化配置poolstring当前 pool 名称会影响服务端如何推断堆栈viteEnvironmentstring当前处理文件的 Vite 环境名getImportDurations/getModuleFetchDuration—模块导入耗时统计服务于 import breakdown 报告extendTaskContext在默认实现里为上下文注入了惰性求值的expect、bench与_local属性test.ts。三、编写你的第一个自定义 Runner结合上面的接口一个最小可用的自定义 runner 长这样源自 docs/api/advanced/runner.mdimport type { RunnerTestFile, SerializedConfig, TestRunner, VitestTestRunner } from vitest class CustomRunner extends TestRunner implements VitestTestRunner { public config: SerializedConfig constructor(config: SerializedConfig) { this.config config } onAfterRunFiles(files: RunnerTestFile[]) { console.log(finished running, files) } } export default CustomRunner要点构造时拿到配置Vitest 实例化 runner 类时会传入序列化配置你必须把它暴露为config属性源码中resolveTestRunner会在缺失时兜底补上但显式声明更清晰。继承TestRunner文档明确建议从vitest导入的TestRunner继承以保留快照支持等依赖 runner 的能力如需扩展基准测试benchmark能力可使用NodeBenchmarkRunner。default 导出加载器只认default导出index.ts。moduleRunner 与 importFileVitest 会向每个 runner 注入vite/module-runner的ModuleRunner实例moduleRunner属性。TestRunner与BenchmarkRunner的默认importFile行为就是委托给它export default class Runner { async importFile(filepath: string) { await this.moduleRunner.import(filepath) } }ModuleRunner.import会在运行时解析导入并转换文件内容让 Node 能直接理解 Vite 生态的模块。默认TestRunner的实现还做了两件事test.ts未启用experimental.viteModuleRunner时为文件路径追加?vitest${Date.now()}查询串以绕过模块缓存在 OpenTelemetry 追踪跨度vitest.module.import_collect/vitest.module.import_setup下执行导入。若你既没有自定义 runner也没有定义runTest/runTask方法Vitest 会尝试自动取回任务如果任务没有通过setFn注册函数运行会直接失败。因此在自定义执行流程时务必确保任务函数已被正确设置TestRunner静态方法中提供了setTestFn/getTestFn等工具见 test.ts。取消机制cancel(reason: CancelReason)会在需要取消后续测试运行时被调用。CancelReason的类型为keyboard-input | test-failure | (string ...)types.ts即键盘中断或测试失败触发。runner 应当监听该方法并在onBeforeRunSuite/onBeforeRunTask中把后续任务标记为skip——这正是默认实现的策略cancelRun标志位见 test.ts 与 onBeforeRunTask。bail配置也是通过rpc().onCancel(test-failure)加上testRunner.cancel(test-failure)触发的index.ts。四、理解 TasksRunner 视角的任务树Suites 与 tests 在内部统称为tasks任务。这一层 API 目前标记为experimental应主要在测试运行时使用如果在主线程例如 reporter 内工作应优先使用 Reported Tasks API——团队正在讨论未来是否用 Reported Tasks 取代 Runner Tasks。File文件的根任务收集任何测试之前runner 会先创建一个File任务——它是Suite的超集额外携带interface File extends Suite { /** 文件所属的 pool 名称默认 forks */ pool?: string /** UNIX 格式的文件路径 */ filepath: string /** 文件所属测试项目名 */ projectName: string | undefined /** 收集该文件所有测试的耗时含导入全部依赖的时间 */ collectDuration?: number /** 导入 setup 文件的耗时 */ setupDuration?: number }Suite 与 Test每个 suite 拥有tasks: Task[]属性在收集阶段被填充适合自上而下遍历任务树interface Suite extends TaskBase { type: suite /** 文件的根任务 */ file: File /** 属于该 suite 的任务数组 */ tasks: Task[] }每个 task 又通过suite属性反向引用其所在套件适合自下而上回溯。注意三条边界规则源码中的TaskBase/Test定义见 types.ts在顶层文件根部声明的test/describe没有suite属性它不等于fileFile本身永远没有suite属性每个 task 的file属性总是指向文件根任务。Test还带有测试上下文与执行辅助字段interface TestExtraContext object extends TaskBase { type: test /** 传给测试函数的上下文 */ context: TestContext ExtraContext /** 文件的根任务 */ file: File /** 是否通过 context.skip() 跳过 */ pending?: boolean /** 期望失败失败时会被标记为通过 */ fails?: boolean /** 存储异步 expect 的 promise在测试结束前等待它们 */ promises?: Promiseany[] }TaskBase上的公共字段还包括稳定的id、name、以连接的fullName/fullTestName、modeskip/only/todo/run/queued、meta、each、concurrent、shuffle、retry/repeats、location、tags等。TaskResult任务的执行结果每个 task 都可以有result字段。Suite 只有在其回调或beforeAll/afterAll抛错导致无法完成收集时才有resultTest 则在其回调执行后总是拥有result其中state与errors依据结果而定。若错误发生在beforeEach/afterEach中错误会出现在task.result.errors里。export interface TaskResult { /** 任务状态。收集期间继承 task.mode结束后变为 pass 或 fail */ state: TaskState /** 执行期间的错误expect.soft() 多次失败时可能有多条 */ errors?: TestError[] /** 任务运行耗时毫秒 */ duration?: number /** 任务开始运行的时间戳毫秒 */ startTime?: number /** 任务结束后的堆大小字节。仅当启用 logHeapUsage 且存在 process.memoryUsage 时可用 */ heap?: number /** 与任务相关的钩子状态便于报告使用 */ hooks?: PartialRecordafterAll | beforeAll | beforeEach | afterEach, TaskState /** 重试次数。仅当任务失败且设置了 retry 时才会重试 */ retryCount?: number /** 重复次数。仅当设置了 repeats 时才会重复该数字包含 retryCount */ repeatCount?: number }五、自定义 Task 函数createTaskCollector 实战Vitest 暴露了createTaskCollector工具用来创建你自己的test方法。它与内置test行为一致但在收集阶段会调用你传入的自定义逻辑。原理getCurrentSuite().task()一个 task 本质上是 suite 中的对象通过suite.task()方法自动加入当前套件源码见 suite.ts其中task方法会合并父套件选项与 tags、计算timeout、创建TestContext、注册 handler并支持meta、concurrent、location等字段。下面复刻文档中的园艺示例。先创建自定义任务收集器export { afterAll, beforeAll, describe, TestRunner } from vitest // 该函数在收集阶段被调用 // 不要在这里直接调用函数 handler而是通过 // getCurrentSuite().task() 方法把它加入套件任务 // 注意createTaskCollector 自动提供 todo/each/... 等链式支持 export const myCustomTask TestRunner.createTaskCollector( function (name, fn, timeout) { TestRunner.getCurrentSuite().task(name, { ...this, // 确保 todo/skip/... 等修饰符被正确跟踪 meta: { customPropertyToDifferentiateTask: true }, handler: fn, timeout, }) } )从源码看createTaskCollectorsuite.ts返回的是一个经由createChainable包装的链式 API内置了concurrent、skip、only、todo、fails修饰符并为每个收集器补齐each、for、skipIf、runIf、extend、override以及describe/suite/beforeEach/afterEach/beforeAll/afterAll/aroundEach/aroundAll等属性——所以你自定义的 task 天然拥有与test等价的完整语法糖。然后在测试文件中使用import { afterAll, beforeAll, describe, myCustomTask } from ./custom.js import { gardener } from ./gardener.js describe(take care of the garden, () { beforeAll(() { gardener.putWorkingClothes() }) myCustomTask(weed the grass, () { gardener.weedTheGrass() }) myCustomTask.todo(mow the lawn, () { gardener.mowerTheLawn() }) myCustomTask(water flowers, () { gardener.waterFlowers() }) afterAll(() { gardener.goHome() }) })运行vitest ./garden/tasks.test.js关键设计要点收集阶段不执行 handlercreateTaskCollector的回调在收集期运行职责是登记任务把handler、timeout、meta交给getCurrentSuite().task()真正的测试函数由运行阶段取出执行。...this透传修饰符skip/only/todo等链式调用产生的标志位于this上展开后传入task()保证myCustomTask.skip(...)、myCustomTask.only(...)行为正确。meta 自定义元数据通过meta字段可以给任务打上自定义标记例如示例中的customPropertyToDifferentiateTaskJSON reporter 会保存这些数据后续可据此做差异化处理。仓库中的单测 test/unit/test/task-collector.test.ts 对这套机制做了直接验证例如验证collector.each([1])(a, cb, options.timeout)与collector.each([1])(a, options, cb)两种签名都能工作、suite.task能继承 suite 的meta/concurrent/repeats/retry/timeout选项、以及空 suite / 空 test 自动降级为 todo的行为empty tests and suites are todos。六、边界与注意事项快照依赖 runner快照支持等特性与 runner 深度耦合默认实现在 test.ts 的onAfterRunSuite中完成快照落盘与废弃检测。如果不希望失去快照能力务必继承TestRunner。必须实现importFile否则resolveTestRunner会直接抛错。必须 default 导出构造函数加载器通过moduleRunner.import(config.runner)取mod.default。RPC 已自动代理onTaskUpdate、onCollected、onAfterRunFiles覆盖率、onAfterRunTaskbail等已被resolveTestRunner包裹自定义 runner 无需也不应自行调用rpc()。Runner Tasks 是实验性 API文档明确提示它应主要用于测试运行时主线程reporter场景请使用 Reported Tasks API二者未来可能合并。runTest/setFn的对应关系如果自定义了执行流程但没有通过setFn给任务注册函数Vitest 自动取回任务时会失败。请使用 TestRunner 暴露的静态工具setTestFn/getTestFn/setSuiteHooks/getSuiteHooks/matchesTags/createFileTask等来管理任务函数与钩子。相关资源本文骨架来源docs/api/advanced/runner.md配置项定义docs/config/runner.md接口与类型定义packages/vitest/src/runtime/runner/types.tsVitestRunner见 L1623-L1773默认运行器实现packages/vitest/src/runtime/runners/test.tsTestRunner类见 L36-L304加载与代理逻辑packages/vitest/src/runtime/runners/index.ts任务收集器实现packages/vitest/src/runtime/runner/suite.tscreateTaskCollector见 L744-L976相关测试test/unit/test/task-collector.test.ts【免费下载链接】vitestNext generation testing framework powered by Vite.项目地址: https://gitcode.com/GitHub_Trending/vi/vitest创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考