☰
用 expect.extend() 为 Jest 定义自定义匹配器:TIL 仓库实战笔记
2026/10/7 20:29:49 网站建设 项目流程
  • 文档
  • 教程
  • 知识库

【免费下载链接】til

:memo: Today I Learned

项目地址:https://gitcode.com/gh_mirrors/ti/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}` }; } } });

拆解这段代码,可以提炼出自定义匹配器的三要素:

  1. 匹配器名称:这里的toHaveMatchingId即匹配器名字。遵循 Jest 惯例,自定义匹配器一般以to开头,便于与内置匹配器保持一致的阅读体验。
  2. 判定逻辑:函数体内的pass计算即核心判定。本例仅比对recieved.id === expected.id,说明匹配器实现完全可以是普通的 JavaScript 运算,没有任何黑魔法。
  3. 两个返回分支: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}` }; } } });

拆解这段代码,可以提炼出自定义匹配器的三要素:

  1. 匹配器名称:这里的toHaveMatchingId即匹配器名字。遵循 Jest 惯例,自定义匹配器一般以to开头,便于与内置匹配器保持一致的阅读体验。
  2. 判定逻辑:函数体内的pass计算即核心判定。本例仅比对recieved.id === expected.id,说明匹配器实现完全可以是普通的 JavaScript 运算,没有任何黑魔法。
  3. 两个返回分支: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

项目地址:https://gitcode.com/gh_mirrors/ti/til
点击查看免费下载

相关推荐

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询