使用resetModules时Jest Spy/Mock函数未正常工作的问题排查
为什么Jest重置模块后,Mock函数的调用记录无法跨测试保留?
问题场景
我有一个维护本地状态的add函数,为了让测试之间完全隔离,在每个测试前重新导入该函数并调用jest.resetModules(),同时通过jest.mock监听它内部调用的logTotal函数。
但实际测试时发现:被mock的logTotal能被正确调用,但调用记录在第二个测试中是空的,导致toHaveBeenCalledTimes(2)的断言失败。虽然每次测试前重新导入logTotal就能解决问题,但我想知道必须这么做的原因。
相关代码
add.js
const { logTotal } = require('./logTotal'); let total = 0; const add = () => { ++total; logTotal(total); return total; }; module.exports = { add, };
logTotal.js
const logTotal = (number) => console.log(number); module.exports = { logTotal, };
add.test.js
const logger = require('./logTotal'); jest.mock('./logTotal', () => { return { logTotal: jest.fn().mockImplementation(() => console.log('hello')), }; }); describe('add', () => { let add; beforeEach(async () => { const addModule = await require('./add'); add = addModule.add; jest.resetModules(); }); afterEach(() => { jest.resetAllMocks(); }); it('should return one if called once', () => { const result = add(); expect(result).toBe(1); // 这里会打印 [ [ 1 ] ] console.log(logger.logTotal.mock.calls); expect(logger.logTotal).toHaveBeenCalledTimes(1); }); it('should return two if called twice', () => { add(); const result = add(); expect(result).toBe(2); // 这里会打印 [] console.log(logger.logTotal.mock.calls); expect(logger.logTotal).toHaveBeenCalledTimes(2); }); });
原因分析
核心问题出在模块缓存机制和Mock函数的实例生命周期上:
jest.mock是模块级别的Mock,会在测试文件执行前就替换./logTotal的导出。你在测试文件顶部导入的logger,指向的是第一次Mock生成的函数实例。- 你在
beforeEach里调用了jest.resetModules(),这个API会清空Node.js的模块缓存。当第二个测试执行时,重新导入add模块时,add会再次加载./logTotal——此时Jest会重新生成一个全新的Mock函数实例(因为模块缓存被清空,Mock模块会被重新初始化)。 - 但你测试中断言用的还是测试文件顶部导入的旧Mock实例,它和新
add模块里调用的logTotal已经不是同一个函数了,所以旧实例的mock.calls自然是空的。
解决方案
方案一:每次测试前重新导入Mock模块
修改beforeEach,在重置模块缓存后重新导入logTotal,确保拿到的是当前测试周期内的Mock实例:
describe('add', () => { let add; let logger; // 把logger移到describe作用域内 beforeEach(async () => { jest.resetModules(); // 先清空模块缓存 logger = require('./logTotal'); // 重新导入Mock的logTotal const addModule = await require('./add'); add = addModule.add; }); afterEach(() => { jest.resetAllMocks(); }); // 测试用例保持不变,断言用当前的logger.logTotal即可 });
方案二:使用jest.spyOn替代模块级Mock
用jest.spyOn来监听原模块的导出,避免模块重新初始化带来的实例不一致:
describe('add', () => { let add; let logTotalSpy; beforeEach(async () => { jest.resetModules(); const logger = require('./logTotal'); // 监听logTotal并替换实现 logTotalSpy = jest.spyOn(logger, 'logTotal').mockImplementation(() => console.log('hello')); const addModule = await require('./add'); add = addModule.add; }); afterEach(() => { jest.resetAllMocks(); }); it('should return one if called once', () => { const result = add(); expect(result).toBe(1); expect(logTotalSpy).toHaveBeenCalledTimes(1); }); it('should return two if called twice', () => { add(); const result = add(); expect(result).toBe(2); expect(logTotalSpy).toHaveBeenCalledTimes(2); }); });
内容的提问来源于stack exchange,提问作者John Marsden
相关产品推荐
相关产品推荐

