
Vitest 自定义断言 Matcher 完全指南使用 expect.extend 扩展你的断言能力【免费下载链接】vitestNext generation testing framework powered by Vite.项目地址: https://gitcode.com/GitHub_Trending/vi/vitest本篇技术指南围绕 Vitest 的expect.extendAPI系统讲解如何在测试中自定义断言 Matcher从最基础的实现与注册、TypeScript 类型声明与增强、同步/异步 Matcher 的返回值协议到this上下文中的全部状态属性与源码级实现原理。读完本文你将能够为团队沉淀一套类型安全、可复用、支持.resolves/.rejects/expect.poll/expect.soft的自定义断言库并理解其与 Chai 插件体系的关系。由于 Vitest 同时兼容 Chai 与 Jest 两套断言生态你可以自由选择chai.use插件 API 或expect.extend来扩展断言——两者底层共用同一套 Chai 插件机制。本文聚焦expect.extend这条路径它对从 Jest 迁移而来的团队尤其友好。快速上手第一个自定义 Matcher调用expect.extend并传入一个包含自定义 Matcher 的对象即可扩展默认断言。每个 Matcher 是一个普通函数接收的第一个参数是expect(...)中的接收值received其余参数是调用时传入的参数expect.extend({ toBeFoo(received) { const { isNot } this return { // 不要根据 isNot 手动翻转 passVitest 会自动处理 pass: received foo, message: () ${received} is${isNot ? not : } foo } } }) // 使用 expect(foo).toBeFoo() expect(bar).not.toBeFoo()关键约定pass为true表示断言通过当与.not组合时Vitest 会自动反转最终结果Matcher 内部无需感知isNotmessage是惰性求值的箭头函数只在断言失败时被调用用于生成报错信息若断言失败message()的返回值会作为错误信息抛出并可携带actual/expected让 Vitest 渲染 diff。底层原理expect.extend 如何生效expect.extend并不是独立于 Chai 的旁路实现而是直接包装为 Chai 插件。在 packages/expect/src/jest-extend.ts 中可以看到JestExtend插件通过utils.addMethod(chai.expect, extend, ...)注册extend方法内部再调用use(JestExtendPlugin(chai, expect, expects))把自定义 Matcher 注入 Chai 的Assertion.prototype。JestExtendPlugin对每个 Matcher 做了三件事包装为__VITEST_EXTEND_ASSERTION__调用时先经getMatcherState组装出this上下文再以expectAssertion.call(state, obj, ...args)调用你的 Matcher通过utils.addMethod同时挂载到JEST_MATCHERS_OBJECT.matchers与c.Assertion.prototype保证expect(received).toBeFoo()可用额外把 Matcher 注册为**非对称匹配器asymmetric matcher**挂到expect.toBeFoo/expect.not.toBeFoo上见 jest-extend.ts所以expect.extend一次声明expect.extend、expect().*、expect.*三处同时生效——这也正是官方文档Extending theMatchersinterface will add a type toexpect.extend,expect().*, andexpect.*methods at the same time这句话的实现来源。此外源码还通过wrapAssertion见 packages/expect/src/utils.ts让自定义 Matcher 自动支持expect.soft模式失败时不再中断当前测试。TypeScript 类型声明让自定义 Matcher 类型安全使用 TypeScript 时需要在一个**环境声明文件ambient declaration file**中扩展vitest模块的Matchers接口例如vitest.d.tsimport vitest declare module vitest { interface MatchersR, T { toBeFoo: () R } }说明R是断言的返回类型T是接收值的类型同步Matcher 返回R普通断言下R解析为void当断言与.resolves、.rejects、expect.poll或expect.element组合使用时自动变为Promisevoid当期望参数应与接收值同类型时使用T例如toEqualTyped: (expected: T) R。::: tip 必须import vitest否则 TypeScript 不会把该文件当作模块处理declare module增强将不生效。 :::::: warning 别忘记把环境声明文件加入tsconfig.json的include列表否则类型提示不会加载。 :::从源码看Matchers接口定义于 packages/expect/src/types.tsinterface MatchersR extends void | Promisevoid void | Promisevoid, T unknown {}。它是一个开放的空接口正是为了让你通过declare module合并声明其类型参数命名必须与扩展时保持一致。ExpectStatic和JestAssertion都继承了Matchers因此扩展一处即全局生效。而这些类型也通过 packages/vitest/src/public/index.ts 从vitest入口重新导出供使用方导入。Matcher 返回值协议SyncMatcherResult 与 MatcherResultMatcher 的返回值必须兼容以下结构interface SyncMatcherResult { pass: boolean message: () string // 如果传入以下字段失败时会自动出现在 diff 中 // 无需你在 message 里手动打印 diff actual?: unknown expected?: unknown meta?: object } type MatcherResult SyncMatcherResult | PromiseSyncMatcherResult实战要点actual/expected强烈建议返回当断言失败时Vitest 会把二者渲染成漂亮的 diff 输出而不是一长串JSON.stringify。在 jest-extend.ts 中失败时构造的JestExtendError会携带actual、expected以及assertionName与meta供报告器与 IDE 展示meta是 Vitest 4.1 起支持的附加元数据对象可携带任意结构化信息类型层面的等价定义是SyncExpectationResult与ExpectationResult见 types.tsRawMatcherFn规定 Matcher 函数签名为(this: T, received: any, ...expected: E): ExpectationResult。异步 Matcher返回 Promise 并正确 await如果 Matcher 实现是异步的例如需要查询数据库、读取文件返回值需要是PromiseSyncMatcherResult类型声明为Promisevoid而非R并且在测试中显式awaitexpect.extend({ async toBeAsyncAssertion(received) { return { pass: received foo, message: () expected ${received} to be foo, } } }) declare module vitest { interface MatchersR, T { toBeAsyncAssertion: () Promisevoid } } await expect(foo).toBeAsyncAssertion()从源码看JestExtendPlugin会检测返回结果是否为 thenabletypeof (result as any).then function若是则走thenable.then(...)的异步分支处理失败抛错见 jest-extend.ts。因此异步 Matcher 与同步 Matcher 的失败处理路径完全一致只是多了 Promise 的等待。4.1 官方导出的 Matcher 类型自 Vitest 4.1 起官方从vitest直接导出了编写自定义 Matcher 所需的类型无需再从chai或jest/expect-utils寻找import type { // 函数类型 Matcher, // 返回值 MatcherResult, // 以 this 暴露的状态 MatcherState, } from vitest import { expect } from vitest // 简单 Matcher用 function 声明以便访问 this const customMatcher: Matcher function (received) { // ... } // 带参数的 Matcher const customMatcher: MatcherMatcherState, [arg1: unknown, arg2: unknown] function (received, arg1, arg2) { // ... } // 带自定义注解、显式 this 的 Matcher function customMatcher(this: MatcherState, received: unknown, arg1: unknown, arg2: unknown): MatcherResult { // ... return { pass: false, message: () something went wrong!, } } expect.extend({ customMatcher })需要说明Matcher是 RawMatcherFn 的别名MatcherResult是ExpectationResult的别名SyncMatcherResult是SyncExpectationResult的别名——导出路径见 packages/vitest/src/public/index.ts三者与上述返回值协议一一对应。::: tip 如果要构建自定义快照 Matcher对toMatchSnapshot()/toMatchInlineSnapshot()/toMatchFileSnapshot()的包装请使用vitest导出的Snapshots详见 Custom Snapshot Matchers。仓库中 test/e2e/snapshots/custom-matcher.test.ts 提供了完整的自定义快照 Matcher 示例。 :::this 上下文MatcherState 全部属性详解Matcher 函数体内可以通过this访问当前断言状态。下表为官方文档列出的核心属性其余状态值由 Vitest 内部使用。对应实现见 getMatcherState 与 MatcherState 接口。isNot当以.not调用expect(received).not.toBeFoo()时为true。无需在 Matcher 内自行处理Vitest 会自动反转pass的最终结果。promise当 Matcher 被resolved/rejected修饰符调用时值为对应修饰符名称如resolved否则为空字符串。equals内部用于几乎所有内置 Matcher 的深度比较工具函数。返回true/false表示两个值是否相等默认支持嵌套的非对称匹配器如expect.any(...)、expect.objectContaining(...)。其类型签名为(a, b, customTesters?, strictCheck?) boolean。utils一组用于格式化与输出断言消息的工具函数例如打印颜色、缩进、构造expect(...).toBe...风格的提示文本等。源码中该集合由getMatcherUtils()展开并追加了diff、stringify、iterableEquality、subsetEquality见 jest-extend.ts便于你在message()中复刻 Vitest 原生的 diff 输出。currentTestName当前测试的完整名称包含 describe 块的嵌套名称。在源码中来自task?.fullTestName从 packages/expect/src/state.ts 的getState机制注入。task4.1.0当可用时包含对 Test runner task 的引用可据此访问当前测试任务的元信息。::: warning 在并发测试中若使用全局expectthis.task为undefined。此时应改用测试上下文中的context.expect确保自定义 Matcher 里能拿到task。 :::这一限制在仓库测试中有直接体现test/e2e/test/expect-task.test.ts 覆盖了全局expect、context.expect以及并发场景下task的可用性差异。testPath当前测试文件路径。environment当前environment的名称例如jsdom、node、happy-dom等。soft断言是否以soft形式调用。同样无需在 Matcher 中自行处理——Vitest 总会捕获软断言错误不会中断后续用例。这由 wrapAssertion 在注册阶段统一包装实现。assertion5.0.0底层 Chai assertion在getMatcherState中通过assertion: assertion as any注入。运行时代码示例一个完整的自定义 Matcher将本文内容组合成一个可直接运行在仓库测试环境中的完整示例参考 test/e2e/test/expect-extend.test.ts 的组织方式import { expect, test, type MatcherState } from vitest expect.extend({ // 自定义校验 received 是偶数 toBeEven(this: MatcherState, received: number) { return { pass: received % 2 0, message: () expected ${received} to be an even number, actual: received, expected: an even number, } }, }) test(even matcher works, () { expect(42).toBeEven() expect(41).not.toBeEven() })配套的vitest.d.ts环境声明import vitest declare module vitest { interface MatchersR { toBeEven: () R } }小结expect.extend与chai.use底层同源通过 JestExtend 插件 注册一次声明同时支持expect.extend、expect().*与expect.*非对称匹配器三种用法Matcher 返回值遵循SyncMatcherResult/MatcherResult协议actual、expected、meta字段会在失败时自动渲染 diff异步 Matcher 记得返回 Promise 并在测试中await类型扩展通过declare module vitest合并开放的 Matchers 接口 完成R与T分别对应断言返回类型与接收值类型this上下文MatcherState提供了isNot、promise、equals、utils、currentTestName、task、testPath、environment、soft、assertion等状态其中task在并发测试的全局expect下不可用应改用context.expect构建快照类自定义 Matcher 时使用vitest导出的Snapshots参考 Custom Snapshot Matchers。至此你已经掌握了从写一个简单 Matcher到类型安全地构建异步、软断言、非对称匹配的自定义断言库的完整链路可以开始为你的项目沉淀专属断言方言了。【免费下载链接】vitestNext generation testing framework powered by Vite.项目地址: https://gitcode.com/GitHub_Trending/vi/vitest创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考