- 文档
- 教程
- 知识库
【免费下载链接】til
:memo: Today I Learned
本文基于 TIL(Today I Learned)仓库中的 define-a-custom-jest-matcher 一篇,系统讲解如何借助 Jest 的expect.extend()定义属于你自己的自定义匹配器(Custom Matcher),并给出一个可复制运行的toHaveMatchingId示例。读完本文,你将掌握匹配器的返回契约(pass与message)、在测试中像内置匹配器一样使用自定义匹配器的方法,以及借助 setup 文件全局注册、支持嵌套匹配等进阶技巧。
为什么需要自定义匹配器
Jest 内置的 expect 匹配器(如toBe、toEqual、toContain)已经能覆盖绝大多数测试场景。但在某些业务场景下,你需要一套领域专属的断言语义:例如只比较两个对象的id字段是否相等,而不关心其余字段;又例如校验一个数组里是否存在满足特定条件的元素。这些需求用内置匹配器往往要写出一长串临时逻辑,可读性差、且无法复用。
此时就可以定义自定义匹配器,把"如何判定通过/失败"以及"失败时输出什么信息"封装在单个可复用的断言里。Jest 为此提供了官方入口 ——expect.extend()函数,本文的核心示例正是围绕它展开的。
认识 expect.extend() 与匹配器的返回契约
expect.extend()接受一个对象,对象的每个键就是一个自定义匹配器的名字,键对应的值是该匹配器的实现函数。匹配器实现函数接收被测值(received,即expect(...)里传入的值)以及你传入的其余参数(本文示例中的expected)。
实现函数必须返回一个对象,该对象包含两个关键字段:
pass: boolean—— 表示本次断言是否通过;message: () => string—— 一个返回字符串的函数,用于在断言失败时生成给测试运行器展示的错误信息。
从源码结构看,Jest 拿到这个返回对象后,会结合断言的否定形式(如not.toHaveMatchingId)决定最终向用户展示message()的哪一侧文案,因此在pass: true分支里通常写的是"预期不匹配却匹配了"这类否定语义的提示。
完整示例:按 id 比较对象的 toHaveMatchingId 匹配器
下面是原文档给出的完整实现 —— 一个只依据id属性判断两个对象是否相等的匹配器:
expect.extend({ toHaveMatchingId(recieved, expected) { const pass = recieved.id === expected.id; if (pass) { return { pass: true, message: () => `expected id:${expected.id} to not match actual id:${recieved.id}` }; } else { return { pass: false, message: () => `expected id:${expected.id} to match actual id:${recieved.id}` }; } } });拆解这段代码,可以提炼出自定义匹配器的三要素:
- 匹配器名称:这里的
toHaveMatchingId即匹配器名字。遵循 Jest 惯例,自定义匹配器一般以to开头,便于与内置匹配器保持一致的阅读体验。 - 判定逻辑:函数体内的
pass计算即核心判定。本例仅比对recieved.id === expected.id,说明匹配器实现完全可以是普通的 JavaScript 运算,没有任何黑魔法。 - 两个返回分支:
pass: true与pass: false各返回一个带message函数的对象。注意pass: true分支的文案是"期望 id:xxx 不匹配实际 id:xxx",它服务于not.toHaveMatchingId()这类否定断言;pass: false分支才是正常失败时展示的"期望 id:xxx 匹配实际 id:xxx"。
另外,参数名(recieved、expected)本身并无特殊约定,你可以随意命名,只要与函数体内的使用保持一致即可。
在测试中像内置匹配器一样使用
定义完成之后,自定义匹配器在测试里的用法与内置匹配器完全一致,直接链式调用即可:
test("compare objects", () => { expect({ id: "001" }).toHaveMatchingId({ id: "001" }); // ✅ 通过 expect({ id: "001" }).toHaveMatchingId({ id: "002" }); // ❌ 失败,输出:expected id:002 to match actual id:001 });可以看到,当断言失败时,Jest 会自动使用我们在message()中构造的文案进行提示,信息量比笼统的"对象不相等"要友好得多。原文档还附带了一个可以在线运行的示例环境(CodeSandbox),读者可以边改边跑,直观验证pass与message在不同输入下的表现。
进阶:让匹配器支持嵌套匹配
自定义匹配器的判定逻辑虽然可以写任意 JavaScript 运算,但直接使用===或普通比较会丢失 Jest 强大的一等公民能力 ——嵌套匹配器(asymmetric matchers)。仓库中的另一篇 support-nested-matching-in-custom-jest-matchers 专门讲解了这个问题。
考虑这样一个场景:你想断言"数组里包含某个满足条件的值",如果只用some(value => value === containedValue),那么像下面这样使用expect.any(Number)就会失败,尽管数组里确实有数字:
expect(['a', 2, true]).toContainValue(expect.any(Number));解决方案是复用 Jest 内部使用的 Jasmine 比较工具equals,它知道如何把原始值(整数、布尔值乃至整个对象)与嵌套的expect匹配器进行比对:
const { equals } = require("expect/build/jasmineUtils"); expect.extend({ toContainValue(receivedArray, containedValue) { const pass = receivedArray.some(value => equals(value, containedValue)); // return formatted pass/not-pass objects with messages return { ... } } });这里也印证了:自定义匹配器的本质是"返回{ pass, message }的判定函数",在判定逻辑内部你可以自由组合普通运算与 Jest 的嵌套匹配能力。
如何全局注册:在 setup 文件中配置
如果项目里多个测试文件都要用到自定义匹配器,逐个文件顶部调用expect.extend()显然不合适。仓库中的 configure-jest-to-run-a-test-setup-file 一篇提供了更优雅的集中式方案:在package.json(或jest.config.js)里配置一个测试 setup 文件,让 Jest 在每个测试运行前先加载它:
// package.json { // ... "jest": { "setupTestFrameworkScriptFile": "<rootDir>src/setupTests.js" } }其中setupTestFrameworkScriptFile指向以<rootDir>(项目根目录)为基准的 setup 文件路径。你只要把本文的expect.extend({ ... })调用放进src/setupTests.js,所有测试文件就能直接使用toHaveMatchingId了。这类 setup 文件同样适用于需要全局配置测试框架适配器的场景(如 Enzyme)。
相关 Jest 技巧速览
围绕"自定义匹配器 + 高效测试",TIL 仓库的 JavaScript 分类下还有多篇可组合使用的笔记:
- mock-a-function-with-return-values-using-jest:用
jest.fn()与mockReturnValue()/mockReturnValueOnce()控制 mock 函数的返回值,适合在自定义匹配器的测试场景里隔离依赖。 - tell-jest-to-focus-on-running-only-one-test:用
test.only()聚焦单个测试块,调试匹配器判定逻辑时非常有用。 - turn-off-console-error-messages-in-a-test:临时用
jest.fn()替换console.error,在刻意测试失败路径时保持输出干净。 - test-coverage-stats-with-jest:用
jest --coverage查看语句/分支/函数/行覆盖率,帮助你发现哪些断言(包括自定义匹配器)还缺少用例覆盖。
小结
自定义匹配器是 Jest 扩展断言能力的第一方入口。通过expect.extend(),你可以:
- 用普通 JavaScript 逻辑定义领域专属的判定规则(如按
id比较对象); - 严格遵守"返回
{ pass, message }"的契约,分别构造通过与否的提示文案; - 在
message()里嵌入实际值,让失败信息具备可读性; - 借助 setup 文件全局注册,配合嵌套匹配器与
equals工具实现更复杂的断言语义。
当内置匹配器表达不了你的业务断言时,写一个自定义匹配器,往往比在测试里堆临时逻辑更清晰、更可复用。
<输出文章> <输出文章>
用 expect.extend() 为 Jest 定义自定义匹配器:TIL 实战笔记
本文基于 TIL(Today I Learned)仓库中的 define-a-custom-jest-matcher 一篇,系统讲解如何借助 Jest 的expect.extend()定义属于你自己的自定义匹配器(Custom Matcher),并给出一个可复制运行的toHaveMatchingId示例。读完本文,你将掌握匹配器的返回契约(pass与message)、在测试中像内置匹配器一样使用自定义匹配器的方法,以及借助 setup 文件全局注册、支持嵌套匹配等进阶技巧。
为什么需要自定义匹配器
Jest 内置的 expect 匹配器(如toBe、toEqual、toContain)已经能覆盖绝大多数测试场景。但在某些业务场景下,你需要一套领域专属的断言语义:例如只比较两个对象的id字段是否相等,而不关心其余字段;又例如校验一个数组里是否存在满足特定条件的元素。这些场景用内置匹配器往往要写出一长串临时逻辑,可读性差且无法复用。
此时就可以定义自定义匹配器,把"如何判定通过/失败"以及"失败时输出什么信息"封装在单个可复用的断言里。Jest 为此提供了官方入口 ——expect.extend()函数,本文的核心示例正是围绕它展开的。
认识 expect.extend() 与匹配器的返回契约
expect.extend()接受一个对象,对象的每个键就是一个自定义匹配器的名字,键对应的值是该匹配器的实现函数。匹配器实现函数接收被测值(received,即expect(...)里传入的值)以及你传入的其余参数(本文示例中的expected)。
实现函数必须返回一个对象,该对象包含两个关键字段:
pass: boolean—— 表示本次断言是否通过;message: () => string—— 一个返回字符串的函数,用于在断言失败时生成给测试运行器展示的错误信息。
从匹配器的工作方式可以推断:Jest 拿到这个返回对象后,会结合断言的否定形式(如not.toHaveMatchingId)决定最终向用户展示message()的哪一侧文案,因此在pass: true分支里通常写的是"预期不匹配却匹配了"这类否定语义的提示。
完整示例:按 id 比较对象的 toHaveMatchingId 匹配器
下面是原文档给出的完整实现 —— 一个只依据id属性判断两个对象是否相等的匹配器:
expect.extend({ toHaveMatchingId(recieved, expected) { const pass = recieved.id === expected.id; if (pass) { return { pass: true, message: () => `expected id:${expected.id} to not match actual id:${recieved.id}` }; } else { return { pass: false, message: () => `expected id:${expected.id} to match actual id:${recieved.id}` }; } } });拆解这段代码,可以提炼出自定义匹配器的三要素:
- 匹配器名称:这里的
toHaveMatchingId即匹配器名字。遵循 Jest 惯例,自定义匹配器一般以to开头,便于与内置匹配器保持一致的阅读体验。 - 判定逻辑:函数体内的
pass计算即核心判定。本例仅比对recieved.id === expected.id,说明匹配器实现完全可以是普通的 JavaScript 运算,没有任何黑魔法。 - 两个返回分支:
pass: true与pass: false各返回一个带message函数的对象。注意pass: true分支的文案是"期望 id:xxx 不匹配实际 id:xxx",它服务于not.toHaveMatchingId()这类否定断言;pass: false分支才是正常失败时展示的"期望 id:xxx 匹配实际 id:xxx"。
另外,参数名(recieved、expected)本身并无特殊约定,你可以随意命名,只要与函数体内的使用保持一致即可。
在测试中像内置匹配器一样使用
定义完成之后,自定义匹配器在测试里的用法与内置匹配器完全一致,直接链式调用即可:
test("compare objects", () => { expect({ id: "001" }).toHaveMatchingId({ id: "001" }); // ✅ 通过 expect({ id: "001" }).toHaveMatchingId({ id: "002" }); // ❌ 失败,输出:expected id:002 to match actual id:001 });可以看到,当断言失败时,Jest 会自动使用我们在message()中构造的文案进行提示,信息量比笼统的"对象不相等"要友好得多。原文档还附带了一个可以在线运行的示例环境,读者可以边改边跑,直观验证pass与message在不同输入下的表现。
进阶:让匹配器支持嵌套匹配
自定义匹配器的判定逻辑虽然可以写任意 JavaScript 运算,但直接使用===或普通比较会丢失 Jest 的重要能力之一 ——嵌套匹配器(asymmetric matchers)。仓库中的另一篇 support-nested-matching-in-custom-jest-matchers 专门讲解了这个问题。
考虑这样一个场景:你想断言"数组里包含某个满足条件的值",如果只用some(value => value === containedValue),那么像下面这样使用expect.any(Number)就会失败,尽管数组里确实有数字:
expect(['a', 2, true]).toContainValue(expect.any(Number));解决方案是复用 Jest 内部使用的 Jasmine 比较工具equals,它知道如何把原始值(整数、布尔值乃至整个对象)与嵌套的expect匹配器进行比对:
const { equals } = require("expect/build/jasmineUtils"); expect.extend({ toContainValue(receivedArray, containedValue) { const pass = receivedArray.some(value => equals(value, containedValue)); // return formatted pass/not-pass objects with messages return { ... } } });这也印证了:自定义匹配器的本质是"返回{ pass, message }的判定函数",在判定逻辑内部你可以自由组合普通运算与 Jest 的嵌套匹配能力。
如何全局注册:在 setup 文件中配置
如果项目里多个测试文件都要用到自定义匹配器,逐个文件顶部调用expect.extend()显然不合适。仓库中的 configure-jest-to-run-a-test-setup-file 一篇提供了更优雅的集中式方案:在package.json(或jest.config.js)里配置一个测试 setup 文件,让 Jest 在每个测试运行前先加载它:
// package.json { // ... "jest": { "setupTestFrameworkScriptFile": "<rootDir>src/setupTests.js" } }其中setupTestFrameworkScriptFile指向以<rootDir>(项目根目录)为基准的 setup 文件路径。你只要把本文的expect.extend({ ... })调用放进src/setupTests.js,所有测试文件就能直接使用toHaveMatchingId了。这类 setup 文件同样适用于需要全局配置测试框架适配器的场景(如 Enzyme)。
相关 Jest 技巧速览
围绕"自定义匹配器 + 高效测试",TIL 仓库的 JavaScript 分类下还有多篇可组合使用的笔记:
- mock-a-function-with-return-values-using-jest:用
jest.fn()与mockReturnValue()/mockReturnValueOnce()控制 mock 函数的返回值,适合在自定义匹配器的测试场景里隔离依赖。 - tell-jest-to-focus-on-running-only-one-test:用
test.only()聚焦单个测试块,调试匹配器判定逻辑时非常有用。 - turn-off-console-error-messages-in-a-test:临时用
jest.fn()替换console.error,在刻意测试失败路径时保持输出干净。 - test-coverage-stats-with-jest:用
jest --coverage查看语句/分支/函数/行覆盖率,帮助你发现哪些断言(包括自定义匹配器)还缺少用例覆盖。
小结
自定义匹配器是 Jest 扩展断言能力的第一方入口。通过expect.extend(),你可以:
- 用普通 JavaScript 逻辑定义领域专属的判定规则(如按
id比较对象); - 严格遵守"返回
{ pass, message }"的契约,分别构造通过与否的提示文案; - 在
message()里嵌入实际值,让失败信息具备可读性; - 借助 setup 文件全局注册,配合嵌套匹配器与
equals工具实现更复杂的断言语义。
当内置匹配器表达不了你的业务断言时,写一个自定义匹配器,往往比在测试里堆临时逻辑更清晰、更可复用。
- 文档
- 教程
- 知识库
【免费下载链接】til
:memo: Today I Learned
相关推荐
用 SCSS 插值(Interpolation)为 CSS 自定义属性赋值:TIL 仓库中的 Sass 实战笔记
用 SCSS 插值(Interpolation)为 CSS 自定义属性赋值:TIL 仓库中的 Sass 实战笔记 CSS 自定义属性(Custom Proper
文档教程知识库如何让微信聊天记录成为你个人AI的成长养分?
如何让微信聊天记录成为你个人AI的成长养分? 你是否想过,那些与家人朋友的日常对话、工作的重要沟通、生活中的点滴分享,不仅仅是简单的文字交流,而是构建你个人AI
jest-dom源码解析:理解自定义匹配器的实现原理
jest dom源码解析:理解自定义匹配器的实现原理 jest dom是一个为Jest测试框架提供自定义DOM匹配器的强大工具库,它让DOM元素的状态测试变得更
测试前端开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考