在 Jest 中 Mock 全局对象方法:`jest.spyOn()` 完整实战指南
在 Jest 中 Mock 全局对象方法:jest.spyOn() 完整实战指南
测试是软件开发流程中的重要一环,也是错误最容易被忽视的地方——尤其是针对 Math、Date 这类全局对象及其方法的 Mock,写得不好往往会给后续排错埋下难以定位的隐患。本指南以 30-seconds-of-code 仓库中的 jest-mock-global-methods.md 为核心,系统讲解使用 jest.spyOn() 优雅地 Mock 全局对象方法、注入固定返回值或自定义实现,并配套完整测试用例与源码级原理说明,帮助你在实际项目中写出可控、可预测、可维护的 Jest 测试。
为什么需要 Mock 全局对象方法
在编写单元测试时,被测代码常常会间接调用 Math.random()、Date.now() 这类全局方法。这些方法有两个共同特征:
- 返回值不可预测:
Math.random()每次调用返回 0(含)到 1(不含)之间的随机浮点数,Date.now()返回当前时间戳,两者都无法在测试中预先得知具体数值。 - 结果影响业务逻辑:随机数、时间戳一旦进入运算(如抽样、排序、限流、超时判断),测试结果就会变得不确定,进而产生"这次通过、下次失败"的脆弱测试(flaky tests)。
原文档明确指出:在 Jest 中 Mock 全局对象方法的最优方式是使用 jest.spyOn()。它能把"读取真实全局方法 + 按需替换行为 + 测试结束后恢复原状"这整套流程统一管理,避免手动备份/还原全局对象带来的遗漏风险。
jest.spyOn() 的基本用法
jest.spyOn() 接收两个参数:目标对象与要 Mock 的方法名,返回一个 mock 函数。返回的 mock 函数可以继续链式调用,注入一个模拟实现或模拟返回值:
jest.spyOn(Math, 'random').mockReturnValue(0.123456789);
jest.spyOn(Date, 'now').mockReturnValue('123456789');
上面两个示例分别 Mock 了 Math.random() 和 Date.now(),并让它们返回固定值。核心流程如下:
jest.spyOn(Math, 'random')在Math对象上安装监听,同时保留原方法引用,供测试结束后恢复;- 链式调用
.mockReturnValue(0.123456789)覆写返回值,让每次调用都返回同一个确定值; - 测试用例结束后,调用
jest.restoreAllMocks()即可把Math.random()、Date.now()恢复为原始实现,避免污染其他测试。
[!NOTE] 注意示例中的写法:
Date.now()本身返回数字时间戳,但.mockReturnValue('123456789')传入的是字符串。Mock 只负责"替换实现",并不会校验返回值类型,是否与真实方法一致取决于你的测试场景——如果想模拟真实行为,应传Date.now()同类型的数字,例如.mockReturnValue(123456789)。
返回的 mock 函数可以做什么
jest.spyOn() 返回的是一个标准 Jest mock 函数(jest.fn() 的实例),因此它拥有 mock 函数全家桶:
.mockReturnValue(value):固定每次调用都返回value;.mockImplementation(fn):用自定义函数fn替换实现,按需计算返回值;.mockReturnValueOnce(value)/.mockImplementationOnce(fn):仅下一次调用生效(一次性 Mock);.mock.calls/.mock.results:断言调用次数、入参与实际返回值;- 配合
expect(spy).toHaveBeenCalledWith(...)等匹配器验证调用细节。
实战一:Mock Math.random() 获得确定性随机数
Math.random() 是测试中最常见的"不确定源"。以仓库中的 random-boolean.md 为例,这段逻辑直接用 Math.random() 的结果判断真假:
const randomBoolean = () => Math.random() >= 0.5;
在没有 Mock 的情况下,randomBoolean() 的结果完全不可预测,根本无法断言。用 jest.spyOn() 固定返回值后,测试变得完全确定:
// spec/randomBoolean.test.js
describe('randomBoolean', () => {
afterEach(() => jest.restoreAllMocks());
it('returns true when Math.random() is above 0.5', () => {
jest.spyOn(Math, 'random').mockReturnValue(0.8);
expect(randomBoolean()).toBe(true);
});
it('returns false when Math.random() is below 0.5', () => {
jest.spyOn(Math, 'random').mockReturnValue(0.2);
expect(randomBoolean()).toBe(false);
});
it('returns true at the exact 0.5 boundary', () => {
jest.spyOn(Math, 'random').mockReturnValue(0.5);
expect(randomBoolean()).toBe(true); // 使用 >= 比较
});
});
结合范围映射函数的深入验证
仓库中的 random-number-or-integer-in-range.md 展示了如何把 Math.random() 的结果映射到任意范围:
const randomNumberInRange = (min, max) => Math.random() * (max - min) + min;
const randomIntegerInRange = (min, max) =>
Math.floor(Math.random() * (max - min + 1)) + min;
这类函数非常适合用 Mock 精确验证边界行为——由于公式是确定性的,只要知道 Math.random() 的输入值,就能手算出期望输出:
describe('randomNumberInRange', () => {
afterEach(() => jest.restoreAllMocks());
it('maps Math.random() to the requested range', () => {
jest.spyOn(Math, 'random').mockReturnValue(0.5);
expect(randomNumberInRange(2, 10)).toBe(6); // 0.5 * (10 - 2) + 2 = 6
});
it('handles the lower bound', () => {
jest.spyOn(Math, 'random').mockReturnValue(0);
expect(randomNumberInRange(2, 10)).toBe(2);
});
it('handles values close to the upper bound', () => {
jest.spyOn(Math, 'random').mockReturnValue(0.999);
expect(randomNumberInRange(2, 10)).toBeCloseTo(9.992, 3);
});
});
用 .mockImplementation() 控制随机序列
有些业务函数内部会多次调用 Math.random(),例如 random-alphanumeric.md 中的随机字符串生成器:
const randomAlphaNumeric = length => {
let s = '';
Array.from({ length }).some(() => {
s += Math.random().toString(36).slice(2);
return s.length >= length;
});
return s.slice(0, length);
};
此时用 .mockReturnValue() 固定单一返回值往往不够,更适合用 .mockImplementation() 注入一个可控的序列:
describe('randomAlphaNumeric', () => {
afterEach(() => jest.restoreAllMocks());
it('produces a deterministic result from a fixed random sequence', () => {
const values = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6];
jest.spyOn(Math, 'random').mockImplementation(() => values.shift());
expect(randomAlphaNumeric(5)).toHaveLength(5);
});
it('trims results longer than the requested length', () => {
jest.spyOn(Math, 'random').mockReturnValue(0.123456789);
expect(randomAlphaNumeric(3).length).toBeLessThanOrEqual(3);
});
});
从源码结构可以推断,每次调用 Math.random().toString(36) 都会产生一段长度不定的 36 进制字符串,some() 会一直追加直到长度达标,最后 slice(0, length) 截断——Mock 固定输入后,整条链路每一步的结果都变得可预测,这正是全局对象 Mock 的核心价值。
实战二:Mock Date.now() 获得固定时间
Date.now() 是另一个典型的不确定源。时间相关逻辑(如 token 过期判断、缓存失效、超时计算)一旦依赖当前时间,测试就会随实际运行时刻漂移。同样用 jest.spyOn() 固定即可:
// 被测代码
const isExpired = timestamp => Date.now() > timestamp;
// 测试用例
describe('isExpired', () => {
afterEach(() => jest.restoreAllMocks());
it('returns true for a timestamp in the past', () => {
jest.spyOn(Date, 'now').mockReturnValue(1700000000000);
expect(isExpired(1699999999999)).toBe(true);
});
it('returns false for a timestamp in the future', () => {
jest.spyOn(Date, 'now').mockReturnValue(1700000000000);
expect(isExpired(1700000000001)).toBe(false);
});
});
借助固定的 Date.now(),你可以把"当前时刻"钉死在任意时间点,从而稳定地测试相对时间的边界行为(刚刚过期、即将过期、恰好相等),无需依赖真实时钟。
mockReturnValue 与 mockImplementation 的选择
原文档特别强调,两种方案都可以让 Mock 方法返回可预测的值,适用于不同场景:
| 方法 | 行为 | 适用场景 |
|---|---|---|
.mockReturnValue(value) |
每次调用都返回同一个值 | 需要固定结果,如确定随机数、固定时间戳、模拟网络响应 |
.mockImplementation(fn) |
每次调用执行 fn 并返回其结果 |
需要按调用次数/入参计算返回值,或维护一个返回值序列 |
.mockReturnValueOnce(value) |
仅下一次调用返回 value |
模拟第一次调用失败、第二次成功等状态机式场景 |
简单优先原则:能用一个固定值表达的测试,优先用 .mockReturnValue();只有需要区分不同调用结果时,才升级到 .mockImplementation()。两者都可以与 Math.random()、Date.now() 这类全局方法组合使用,让测试在任何机器、任何时刻运行都得到一致结果。
最佳实践:测试后务必恢复 Mock
jest.spyOn() 虽然优雅,但也引入了"全局状态污染"的隐患。Mock 一旦安装就会持续生效,若不在测试结束后恢复,会悄悄影响后续用例甚至其他测试文件。推荐做法是使用 afterEach 钩子统一恢复:
afterEach(() => {
jest.restoreAllMocks();
});
jest.restoreAllMocks() 会一次性恢复所有通过 jest.spyOn() 创建的 Mock(包括 Math.random()、Date.now() 这类全局方法),比逐个调用 .mockRestore() 更省心、更不易遗漏。
恢复 Mock 的三种方式对比
mockFn.mockRestore():恢复被 spy 的原始实现,只作用于单个 mock;jest.restoreAllMocks():批量恢复全部由jest.spyOn()创建的 mock,推荐在afterEach中统一调用;jest.clearAllMocks()/mockFn.mockClear():仅清空mock.calls、mock.results等调用记录,不恢复原始实现,适用于需要保留 mock 但重置计数的场景。
[!WARNING] 全局对象方法被 Mock 后,如果测试过程中被测代码恰好调用了真实逻辑(例如被测函数内部捕获并重试了
Math.random()),务必确保每个用例的 Mock 生命周期完整:先安装、用完后恢复。这也是把jest.restoreAllMocks()放进afterEach而不是手动逐用例清理的原因。
仓库中的关联实践
本仓库本身就是 JavaScript 测试相关内容的集散地,围绕"全局对象方法与随机性"展开的切片还包括:
- random-boolean.md:基于
Math.random() >= 0.5的随机布尔生成器,是练习全局对象 Mock 的最小示例; - random-number-or-integer-in-range.md:将
Math.random()映射到任意数值范围,可用于验证 Mock 边界值; - random-alphanumeric.md:多次调用
Math.random()生成随机字符串,适合演练.mockImplementation()注入序列; - test-driven-development-intro.md:介绍 TDD 方法论与 Vitest 测试写法,可了解不同测试框架下
expect、mock 的异同; - nodejs-test-module-introduction.md:介绍 Node.js 内置测试模块,方便对比 Jest 之外的测试方案。
总结
在 Jest 中 Mock 全局对象方法,核心就是记住一条主线:jest.spyOn(全局对象, '方法名') 拿到 mock 函数,再按需链式调用 .mockReturnValue() 或 .mockImplementation() 注入确定行为,测试结束后用 jest.restoreAllMocks() 恢复现场。这套组合拳足以覆盖 Math.random()、Date.now() 等绝大多数全局对象方法的测试场景,让你的测试摆脱随机性与时间漂移的干扰,做到可预测、可复现、可维护。