测试不纯函数的关键是控制外部状态而非强求纯化:先识别隐式依赖(如Date、localStorage等),再通过模块mock、全局对象临时替换或参数注入实现可控;对真实副作用做轻量mock并断言其行为。

直接测试依赖外部状态的不纯函数,关键不是让它“变纯”,而是控制它所依赖的外部状态,让每次测试运行在可预测、可重置的环境中。
识别并隔离外部依赖源
先明确函数依赖了哪些外部状态:全局变量、localStorage、sessionStorage、Date、Math.random()、模块级常量或单例对象等。这些都不是参数传入的,而是隐式读取的——这正是不可测的根源。
- 用
jest.mock()或vi.mock()拦截整个模块(如模拟date-fns或自定义工具模块) - 对全局对象做局部替换:测试前保存原始值,用
Object.defineProperty(global, 'Date', {...})临时覆盖;测试后恢复 - 避免 patch
window或globalThis全局,优先在函数调用链上游注入可控依赖(见下一条)
把隐式依赖显式化,再注入
重构是提升可测性的最有效方式。不强行 mock 全局,而是修改函数签名,把外部状态作为参数传入:
使用约定式提交(Conventional Commits)生成标准化的 Git 提交信息。适用于以下场景: (1) 暂存更改时需要提交信息; (2) 审查 diff 时希望获得结构化摘要; (3) 编写 PR 描述或变更日志(changelog)条目; (4) 用户明确要求“为本次更改编写提交信息”。 该功能会分析已暂存/未暂存的 diff,并生成按类型(type)划分范围的提交信息。
- 原函数:
function getExpiryLabel() { return formatDistanceToNow(new Date()); } - 改造后:
function getExpiryLabel(now = new Date()) { return formatDistanceToNow(now); } - 测试时传入固定时间:
getExpiryLabel(new Date('2026-01-01')),输出完全确定 - 同理处理随机数:
randomId(seed = Math.random());处理存储:loadConfig(storage = localStorage)
针对真实副作用做轻量级行为 Mock
当无法重构(如遗留代码或第三方封装),就只 mock 实际被调用的行为,不模拟整个环境:
- 模拟
localStorage:用内存对象替代,测试前后清空 - 模拟
fetch:用jest.mock('node-fetch')或 MSW 拦截请求,只返回该测试需要的状态码和 body - 模拟定时器:
jest.useFakeTimers()+jest.runAllTimers(),避免等待真实毫秒 - 不 mock 整个浏览器 API 集合,只 mock 当前测试路径中真正触发的那 1–2 个调用点
验证副作用是否按预期发生
不纯函数的价值常在于它做了什么,而不只是返回什么。测试时要断言其“动作”是否正确:
- 检查
localStorage.setItem是否被以指定 key/value 调用过(expect(localStorage.setItem).toHaveBeenCalledWith('token', 'abc')) - 确认
console.error在异常路径中被调用一次 - 断言事件是否被 dispatch:
expect(mockDispatch).toHaveBeenCalledWith({ type: 'USER_LOGIN_FAIL' }) - 注意:这些断言对象本身需在测试前被
jest.fn()替换,并确保未被其他测试污染










