5月27日 19:51

Jest 中有哪些测试匹配器(Matchers)?如何使用自定义匹配器?

为什么匹配器是 Jest 测试的核心

写测试本质上就是做断言——拿实际结果和期望结果比对。匹配器(Matchers)就是 Jest 提供的断言语言,决定了你能用多自然、多精确的方式表达"我期望这段代码的行为是什么"。

如果你只会 toBetoEqual,很多场景要么写不出断言,要么写得很别扭。掌握完整的匹配器体系,加上自定义匹配器的能力,才能写出既清晰又健壮的测试。


相等性匹配器:判断值是否如你所料

最基础也是用得最多的一组:

  • toBe(value) — 严格相等,即 ===。适合原始类型(number、string、boolean)和 null/undefined 的比较。注意:对象比较的是引用,不是内容。
javascript
expect(1 + 1).toBe(2); expect(null).toBe(null);
  • toEqual(value) — 深度递归比较。对象和数组逐字段比对,是测试复杂数据结构的首选。
javascript
expect({ name: 'Jest', version: 29 }).toEqual({ name: 'Jest', version: 29 }); // 通过:内容一致即可,不要求同一引用
  • toStrictEqual(value) — 比 toEqual 更严格:undefined 属性、稀疏数组空位、Date 实例等都会纳入比较。当你需要确保数据结构完全一致、没有多余属性时使用。
javascript
expect({ a: undefined, b: 1 }).not.toStrictEqual({ b: 1 }); // toEqual 会认为两者相同,toStrictEqual 不会
  • toMatchObject(object) — 部分匹配,只检查给定的属性是否存在且值相等,忽略对象中的其他属性。适合只关心几个关键字段的场景。
javascript
const user = { id: 1, name: 'Alice', email: 'alice@test.com', role: 'admin' }; expect(user).toMatchObject({ name: 'Alice', role: 'admin' }); // 只验证这两个字段,其余忽略

真值匹配器:处理 null、undefined 和真假值

JavaScript 的真假值规则经常让人踩坑,Jest 专门提供了一组匹配器:

匹配器通过条件典型用途
toBeNull()null区分 null 和 undefined
toBeUndefined()undefined检测未赋值变量
toBeDefined()undefined确认变量已定义
toBeTruthy()真值(!!value === true检查非空字符串、非零数字等
toBeFalsy()假值(0''nullundefinedfalse检查空值或无数据状态
javascript
// 常见场景:函数返回 null 表示未找到 expect(findUser(-1)).toBeNull(); // 常见场景:检查可选配置项是否存在 expect(config.timeout).toBeDefined(); // 常见场景:检查有内容(非空字符串、非零数字) expect(response.body).toBeTruthy();

一个常见的坑:toBeTruthy()0 和空字符串返回 false。如果你确实需要区分 0undefined,别用 toBeTruthy,用 toBeDefined


数字匹配器:比较大小和精度

  • toBeGreaterThan(n) / toBeGreaterThanOrEqual(n) — 大于 / 大于等于
  • toBeLessThan(n) / toBeLessThanOrEqual(n) — 小于 / 小于等于
  • toBeCloseTo(n, precision) — 浮点数近似比较,避免精度问题
javascript
expect(0.1 + 0.2).not.toBe(0.3); // JavaScript 浮点精度问题 expect(0.1 + 0.2).toBeCloseTo(0.3, 5); // 正确做法:指定精度比较

toBeCloseTo 是处理浮点运算的必备匹配器,第二个参数是小数点后的精度位数,默认是 2。如果测试中涉及金额计算或科学计算,务必用它替代 toBe


字符串匹配器

  • toMatch(regexp | string) — 匹配正则或包含子串
  • toContain(item) — 包含子字符串
javascript
expect('Hello, Jest!').toMatch(/jest/i); expect('error: file not found').toContain('error');

toMatch 支持正则,比 toContain 更灵活。需要模式匹配时用 toMatch,只需判断是否包含子串时用 toContain


数组匹配器

  • toContain(item) — 数组中是否包含某元素(用 === 比较)
  • toContainEqual(item) — 数组中是否包含深度相等的元素
  • toHaveLength(n) — 数组/字符串长度
javascript
const users = [{ id: 1 }, { id: 2 }]; expect(users).toContainEqual({ id: 1 }); // 深度比较,通过 expect(users).not.toContain({ id: 1 }); // 引用比较,不通过 expect(users).toHaveLength(2);

toContain 对对象用的是引用比较,如果数组里存的是对象字面量,一定要用 toContainEqual,否则断言会失败。


对象匹配器

  • toHaveProperty(keyPath, value?) — 检查对象是否有指定属性路径,可选检查值
  • toMatchObject(object) — 部分匹配(上文已介绍)
javascript
const config = { db: { host: 'localhost', port: 5432 } }; expect(config).toHaveProperty('db.port', 5432); // 支持点号路径 expect(config).toHaveProperty(['db', 'host']); // 也支持数组路径

toHavePropertykeyPath 参数支持点号分隔的字符串或字符串数组,可以深层数据校验。


函数匹配器:验证函数调用行为

这组匹配器配合 jest.fn()jest.spyOn() 使用,是 Mock 测试的核心工具:

  • toHaveBeenCalled() — 函数被调用过
  • toHaveBeenCalledWith(...args) — 用特定参数调用过
  • toHaveBeenCalledTimes(n) — 调用了 n 次
  • toHaveLastReturnedWith(value) — 最后一次返回值
  • toHaveNthReturnedWith(n, value) — 第 n 次返回值
  • toHaveReturned() — 成功返回过(没抛错)
  • toHaveReturnedWith(value) — 返回过指定值
javascript
const onClick = jest.fn(); button.click(); button.click(); expect(onClick).toHaveBeenCalledTimes(2); expect(onClick).toHaveBeenCalledWith(); // 无参数调用 // 带参数的场景 const save = jest.fn(); save({ name: 'Alice' }); expect(save).toHaveBeenCalledWith({ name: 'Alice' });

一个实用技巧:toHaveBeenCalledWith 只检查某一次调用是否匹配,不要求所有调用都匹配。如果需要验证所有调用的参数序列,可以用 expect(fn.mock.calls).toEqual([[arg1], [arg2]])


异常匹配器:测试错误抛出

  • toThrow(error?) — 函数抛出错误,可匹配错误消息或类型
  • toThrowErrorMatchingSnapshot() — 错误消息快照
javascript
function divide(a, b) { if (b === 0) throw new Error('Division by zero'); return a / b; } expect(() => divide(1, 0)).toThrow('Division by zero'); expect(() => divide(1, 0)).toThrow(/zero/); expect(() => divide(1, 0)).toThrow(Error);

关键点:toThrow 的参数必须是包裹在函数中的(expect(() => fn()) 而不是 expect(fn())),否则错误会在 expect 执行前直接抛出,测试框架捕获不到。


否定匹配器:用 .not 取反

所有匹配器都可以通过 .not 前缀取反:

javascript
expect(value).not.toBe(42); expect(array).not.toContain('deprecated'); expect(fn).not.toHaveBeenCalled();

.not 链式调用让断言的语义更自然。当 not 加上语义明确的匹配器仍不够用时,就是自定义匹配器登场的时候了。


快照匹配器:捕获和比对输出

  • toMatchSnapshot(propertyMatchers?, hint?) — 与存储的快照比对
  • toThrowErrorMatchingSnapshot() — 异常消息快照
javascript
expect(component.render()).toMatchSnapshot(); // 首次运行会生成快照文件,后续运行自动比对 // 输出变化时测试失败,需用 --updateSnapshot 更新

快照适合测试稳定的序列化输出(如组件渲染结果、配置对象)。不适合频繁变化的数据,否则快照文件会不断需要更新,失去测试价值。


异步匹配器:处理 Promise

  • resolves — 期望 Promise 成功 resolve
  • rejects — 期望 Promise 被 reject
javascript
// 测试异步函数成功返回 await expect(fetchUser(1)).resolves.toEqual({ id: 1, name: 'Alice' }); // 测试异步函数抛错 await expect(fetchUser(-1)).rejects.toThrow('User not found');

使用 resolves / rejects 时必须加 await,否则 Jest 无法正确捕获异步结果,测试会提前结束并始终通过。


自定义匹配器:让断言更贴合业务语义

当内置匹配器无法精确表达你的断言意图时,expect.extend() 允许你创建自己的匹配器。

基本结构

自定义匹配器接收 receivedexpect() 传入的值)和自定义参数,返回一个包含 passmessage 的对象:

javascript
expect.extend({ toBeWithinRange(received, floor, ceiling) { const pass = received >= floor && received <= ceiling; return { pass, message: () => pass ? `Expected ${received} NOT to be within range ${floor}${ceiling}` : `Expected ${received} to be within range ${floor}${ceiling}`, }; }, }); test('score is within passing range', () => { expect(85).toBeWithinRange(60, 100); expect(30).not.toBeWithinRange(60, 100); });

message 函数要同时处理通过和不通过两种场景。passtrue 时,message 描述的是 .not 取反后的预期(因为 .not 让通过的变成失败),反之亦然。

在 TypeScript 项目中使用

自定义匹配器需要扩展 jest.Matchers 接口,否则 TypeScript 会报类型错误:

typescript
// 在 jest.d.ts 或 global.d.ts 中声明 declare global { namespace jest { interface Matchers<R> { toBeWithinRange(floor: number, ceiling: number): R; } } }

实际案例:验证日期范围

javascript
expect.extend({ toBeDateAfter(received, baseline) { const pass = received instanceof Date && baseline instanceof Date && received > baseline; return { pass, message: () => pass ? `Expected ${received.toISOString()} NOT to be after ${baseline.toISOString()}` : `Expected ${received.toISOString()} to be after ${baseline.toISOString()}`, }; }, }); test('expiry date is after creation date', () => { const created = new Date('2025-01-01'); const expires = new Date('2026-01-01'); expect(expires).toBeDateAfter(created); });

自定义匹配器的最佳实践

  1. 命名要语义化toBeValidEmailtoMatchEmailRegex 更易读,测试代码读起来像自然语言。
  2. 输入校验不能省:对 received 做类型检查,遇到非法输入抛出有意义的错误,而不是返回莫名其妙的 pass: false
  3. 配合 setupFilesAfterEnv 全局注册:把 expect.extend() 放在独立的 setup 文件中,在 Jest 配置的 setupFilesAfterEnv 里引入,避免每个测试文件重复注册。
  4. 优先组合内置匹配器:如果只是 expect(a).toBeGreaterThan(x)expect(a).toBeLessThan(y) 的组合,直接用 .and 或写两行断言就够了,不必自定义。自定义匹配器的价值在于表达内置匹配器无法简洁描述的业务规则。

选择匹配器的思路

遇到断言需求时,按这个顺序选择:

  1. 值比较toBe / toEqual / toStrictEqual
  2. 类型或存在性toBeDefined / toBeNull / toBeTruthy
  3. 大小或范围toBeGreaterThan / toBeCloseTo
  4. 包含关系toContain / toContainEqual / toMatchObject
  5. 函数行为toHaveBeenCalledWith / toThrow
  6. 异步结果resolves / rejects
  7. 内置都不合适expect.extend() 自定义

匹配器选对了,测试的可读性和维护性都会上一个台阶。不必死记硬背所有匹配器,理解每个类别的适用场景,需要时查阅即可。自定义匹配器则是把反复出现的断言模式封装成可复用工具,在项目规模变大时尤其值得投入。

标签:Jest