math.random 在 jest 中直接 mock 失效,因其为只读属性,需用 object.defineproperty 配合 writable: true 劫持,并在 beforeeach 设置、aftereach 恢复;推荐封装 getrandom 函数便于注入和测试。

为什么直接 mock Math.random 在 Jest 里会失效
因为 Math.random 是只读属性,Jest 的 jest.mock 或 jest.spyOn 默认无法覆盖它——直接赋值会静默失败(非严格模式下)或报 TypeError: Cannot assign to read only property。很多测试跑着跑着“看起来通过”,其实是没真正 mock 成功,随机逻辑还在偷偷运行。
- 必须用
Object.defineProperty配合writable: true才能临时劫持 - mock 要放在
beforeEach里,且每次测试后需在afterEach恢复原生行为,否则污染后续测试 - 注意 Node.js 环境下
global.Math和浏览器环境一致,但某些测试 runner(如 Vitest)可能有额外沙箱机制,需确认是否启用globals: true
Jest 中稳定替换 Math.random 的三步写法
不依赖第三方库,纯原生 JS 就能搞定。核心是绕过只读限制 + 精确控制返回值序列。
- 在
beforeEach中用Object.defineProperty(global.Math, 'random', { value: jest.fn(), writable: true }) - 给 mock 函数设定返回值:比如
Math.random.mockReturnValueOnce(0.123).mockReturnValueOnce(0.456) - 务必在
afterEach中调用Math.random.mockRestore(),否则下次测试可能拿到上一轮残留的 mock 实例
示例:
beforeEach(() => {
Object.defineProperty(global.Math, 'random', {
value: jest.fn(),
writable: true,
});
});
it('uses fixed random sequence', () => {
Math.random.mockReturnValueOnce(0.2).mockReturnValueOnce(0.8);
expect(myRandomFunction()).toBe('high');
});
用封装函数替代直接调用 Math.random 更易测试
如果业务代码里散落着几十个 Math.random(),逐个 patch 不现实。更可持续的做法是:把随机能力抽成可注入的依赖。
- 定义一个默认导出函数,比如
export const getRandom = () => Math.random() - 业务代码里统一调用
getRandom(),而不是裸写Math.random() - 测试时用
jest.mock('./random', () => ({ getRandom: jest.fn().mockReturnValue(0.7) })) - 这样既避免污染全局,又让随机逻辑可被 DI 替换(比如未来换成真随机服务)
Node.js 里用 crypto.randomInt 怎么 mock
crypto.randomInt 是可写的普通函数,mock 比 Math.random 简单,但容易忽略它的参数行为和错误路径。
- 直接
jest.mock('crypto')后,require('crypto').randomInt可被完全替换 - 注意它支持两种签名:
randomInt(max)和randomInt(min, max),mock 时要按实际调用方式返回对应范围值 - 若原逻辑有
try/catch处理RangeError,测试中应主动触发异常:比如randomInt.mockImplementation(() => { throw new RangeError('invalid range'); })
示例:
jest.mock('crypto', () => ({
randomInt: jest.fn().mockReturnValue(42),
}));
测试中最容易被忽略的不是“怎么 mock”,而是“什么时候恢复”——漏掉 mockRestore 或忘记重置模块缓存,会导致看似无关的测试突然失败,而且错误堆栈根本不提随机数的事。










