Jest 中有哪些测试匹配器(Matchers)?如何使用自定义匹配器?
为什么匹配器是 Jest 测试的核心
写测试本质上就是做断言——拿实际结果和期望结果比对。匹配器(Matchers)就是 Jest 提供的断言语言,决定了你能用多自然、多精确的方式表达"我期望这段代码的行为是什么"。
如果你只会 toBe 和 toEqual,很多场景要么写不出断言,要么写得很别扭。掌握完整的匹配器体系,加上自定义匹配器的能力,才能写出既清晰又健壮的测试。
相等性匹配器:判断值是否如你所料
最基础也是用得最多的一组:
toBe(value)— 严格相等,即===。适合原始类型(number、string、boolean)和null/undefined的比较。注意:对象比较的是引用,不是内容。
javascriptexpect(1 + 1).toBe(2); expect(null).toBe(null);
toEqual(value)— 深度递归比较。对象和数组逐字段比对,是测试复杂数据结构的首选。
javascriptexpect({ name: 'Jest', version: 29 }).toEqual({ name: 'Jest', version: 29 }); // 通过:内容一致即可,不要求同一引用
toStrictEqual(value)— 比toEqual更严格:undefined属性、稀疏数组空位、Date实例等都会纳入比较。当你需要确保数据结构完全一致、没有多余属性时使用。
javascriptexpect({ a: undefined, b: 1 }).not.toStrictEqual({ b: 1 }); // toEqual 会认为两者相同,toStrictEqual 不会
toMatchObject(object)— 部分匹配,只检查给定的属性是否存在且值相等,忽略对象中的其他属性。适合只关心几个关键字段的场景。
javascriptconst 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、''、null、undefined、false) | 检查空值或无数据状态 |
javascript// 常见场景:函数返回 null 表示未找到 expect(findUser(-1)).toBeNull(); // 常见场景:检查可选配置项是否存在 expect(config.timeout).toBeDefined(); // 常见场景:检查有内容(非空字符串、非零数字) expect(response.body).toBeTruthy();
一个常见的坑:toBeTruthy() 对 0 和空字符串返回 false。如果你确实需要区分 0 和 undefined,别用 toBeTruthy,用 toBeDefined。
数字匹配器:比较大小和精度
toBeGreaterThan(n)/toBeGreaterThanOrEqual(n)— 大于 / 大于等于toBeLessThan(n)/toBeLessThanOrEqual(n)— 小于 / 小于等于toBeCloseTo(n, precision)— 浮点数近似比较,避免精度问题
javascriptexpect(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)— 包含子字符串
javascriptexpect('Hello, Jest!').toMatch(/jest/i); expect('error: file not found').toContain('error');
toMatch 支持正则,比 toContain 更灵活。需要模式匹配时用 toMatch,只需判断是否包含子串时用 toContain。
数组匹配器
toContain(item)— 数组中是否包含某元素(用===比较)toContainEqual(item)— 数组中是否包含深度相等的元素toHaveLength(n)— 数组/字符串长度
javascriptconst 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)— 部分匹配(上文已介绍)
javascriptconst config = { db: { host: 'localhost', port: 5432 } }; expect(config).toHaveProperty('db.port', 5432); // 支持点号路径 expect(config).toHaveProperty(['db', 'host']); // 也支持数组路径
toHaveProperty 的 keyPath 参数支持点号分隔的字符串或字符串数组,可以深层数据校验。
函数匹配器:验证函数调用行为
这组匹配器配合 jest.fn() 或 jest.spyOn() 使用,是 Mock 测试的核心工具:
toHaveBeenCalled()— 函数被调用过toHaveBeenCalledWith(...args)— 用特定参数调用过toHaveBeenCalledTimes(n)— 调用了 n 次toHaveLastReturnedWith(value)— 最后一次返回值toHaveNthReturnedWith(n, value)— 第 n 次返回值toHaveReturned()— 成功返回过(没抛错)toHaveReturnedWith(value)— 返回过指定值
javascriptconst 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()— 错误消息快照
javascriptfunction 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 前缀取反:
javascriptexpect(value).not.toBe(42); expect(array).not.toContain('deprecated'); expect(fn).not.toHaveBeenCalled();
.not 链式调用让断言的语义更自然。当 not 加上语义明确的匹配器仍不够用时,就是自定义匹配器登场的时候了。
快照匹配器:捕获和比对输出
toMatchSnapshot(propertyMatchers?, hint?)— 与存储的快照比对toThrowErrorMatchingSnapshot()— 异常消息快照
javascriptexpect(component.render()).toMatchSnapshot(); // 首次运行会生成快照文件,后续运行自动比对 // 输出变化时测试失败,需用 --updateSnapshot 更新
快照适合测试稳定的序列化输出(如组件渲染结果、配置对象)。不适合频繁变化的数据,否则快照文件会不断需要更新,失去测试价值。
异步匹配器:处理 Promise
resolves— 期望 Promise 成功 resolverejects— 期望 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() 允许你创建自己的匹配器。
基本结构
自定义匹配器接收 received(expect() 传入的值)和自定义参数,返回一个包含 pass 和 message 的对象:
javascriptexpect.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 函数要同时处理通过和不通过两种场景。pass 为 true 时,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; } } }
实际案例:验证日期范围
javascriptexpect.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); });
自定义匹配器的最佳实践
- 命名要语义化:
toBeValidEmail比toMatchEmailRegex更易读,测试代码读起来像自然语言。 - 输入校验不能省:对
received做类型检查,遇到非法输入抛出有意义的错误,而不是返回莫名其妙的pass: false。 - 配合
setupFilesAfterEnv全局注册:把expect.extend()放在独立的 setup 文件中,在 Jest 配置的setupFilesAfterEnv里引入,避免每个测试文件重复注册。 - 优先组合内置匹配器:如果只是
expect(a).toBeGreaterThan(x)和expect(a).toBeLessThan(y)的组合,直接用.and或写两行断言就够了,不必自定义。自定义匹配器的价值在于表达内置匹配器无法简洁描述的业务规则。
选择匹配器的思路
遇到断言需求时,按这个顺序选择:
- 值比较 —
toBe/toEqual/toStrictEqual - 类型或存在性 —
toBeDefined/toBeNull/toBeTruthy - 大小或范围 —
toBeGreaterThan/toBeCloseTo - 包含关系 —
toContain/toContainEqual/toMatchObject - 函数行为 —
toHaveBeenCalledWith/toThrow - 异步结果 —
resolves/rejects - 内置都不合适 —
expect.extend()自定义
匹配器选对了,测试的可读性和维护性都会上一个台阶。不必死记硬背所有匹配器,理解每个类别的适用场景,需要时查阅即可。自定义匹配器则是把反复出现的断言模式封装成可复用工具,在项目规模变大时尤其值得投入。