You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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函数的实例生命周期上:

  1. jest.mock是模块级别的Mock,会在测试文件执行前就替换./logTotal的导出。你在测试文件顶部导入的logger,指向的是第一次Mock生成的函数实例。
  2. 你在beforeEach里调用了jest.resetModules(),这个API会清空Node.js的模块缓存。当第二个测试执行时,重新导入add模块时,add会再次加载./logTotal——此时Jest会重新生成一个全新的Mock函数实例(因为模块缓存被清空,Mock模块会被重新初始化)。
  3. 但你测试中断言用的还是测试文件顶部导入的旧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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.11 06:35:31