js单元测试mock循环引用的核心是控制引用链生成时机与生命周期;需拆解源头、模块级mock、对象级weakmap隔离、避免闭包捕获、用快照替代深度比对。

在 JS 单元测试中 Mock 复杂循环引用依赖,核心不是“绕过”它,而是控制引用链的生成时机与生命周期,避免测试环境因真实模块加载顺序或对象结构引发无限递归、undefined 访问或内存泄漏。
拆解循环依赖源头,优先 Mock 模块级引用
CommonJS 或 ESM 中的模块循环依赖(如 A → require B,B → require A)在测试时往往表现为 TypeError: Cannot read property 'xxx' of undefined。这不是代码逻辑错误,而是模块导出未就绪。
- 用
jest.mock()或vi.mock()对涉及循环的模块做自动模拟,让它们返回可控的空对象或桩函数 - 对关键导出项手动指定返回值,例如:
jest.mock('./logger', () => ({ info: jest.fn() })) - 避免在
beforeEach中require循环模块——改用jest.isolateModules隔离加载上下文
对象级循环引用:用 WeakMap + 懒初始化隔离测试实例
当被测对象内部持有自身引用(如 node.parent = parent; parent.children.push(node)),直接构造实例会触发深拷贝失败或栈溢出。
将一个或多个视频上传至YouTube。当用户想“上传到YouTube”“发YouTube”“批量上传”“upload to YouTube”“post videos to YouTube”或发布到YouTube时使用此技能。
- 测试中不 new 真实类,而是用
Object.assign({}, mockBase, { self: null })手动切断闭环 - 若必须保留结构,用
WeakMap在测试作用域内缓存“已初始化对象”,在构造器中查表并复用,防止递归进入 - 对含循环的对象做浅层断言(如只检查
obj.id和obj.name),跳过obj.parent等引用字段的深度比对
避免闭包捕获引发的隐式循环
Mock 回调函数时,若闭包里引用了测试作用域的大对象(如组件实例、store),而该对象又持有了这个回调,就会形成测试专属的循环引用,导致 Jest 内存占用飙升甚至超时。
- 用
jest.fn().mockImplementation(() => {...})替代箭头函数闭包,减少外部变量捕获 - 在
afterEach中显式清空闭包持有的引用:mockCallback.mockClear(); mockCallback.mockReset(); - 对全局状态(如
window.xxx或global.xxx)做jest.resetModules()+jest.clearAllMocks()组合清理
用结构化快照 + 自定义序列化规避深比较陷阱
Jest 默认的 toEqual 会对对象做深度遍历,遇到循环引用直接抛 Converting circular structure to JSON。
- 改用
expect(result).toMatchSnapshot(),Jest 内置对循环引用的序列化处理(显示[Circular]) - 自定义
expect.addSnapshotSerializer,用structuredClone(Node ≥17.0)或带 WeakMap 缓存的手写 clone 函数预处理输出 - 对关键路径做属性级断言:
expect(result.config).toHaveProperty('timeout', 5000),避开整个对象比对










