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

为何单个文件的sinon.restore()会还原其他文件的顶级Stub?

问题原因分析与解决方案

问题根源

  1. Node.js模块缓存机制:Node.js会缓存已加载的模块,所有require('../Service2')调用都会返回同一个内存实例。Service1测试在顶级作用域创建的Stub,直接修改了这个缓存实例上的func2方法。
  2. sinon.restore()的全局特性:sinon.restore()是全局方法,会恢复所有由Sinon创建的Stub、Spy和Mock,不受测试文件边界限制。如果另一个测试文件的afterEach钩子先执行并调用sinon.restore(),会把Service1测试中顶级作用域创建的Stub一并还原。
  3. 测试执行顺序与顶级代码加载时机:测试运行时,所有测试文件的顶级代码会先于测试用例执行。若其他测试先运行,其afterEach会提前还原Service2的func2,等到Service1测试执行时,Service1已通过缓存拿到还原后的Service2实例,自然会调用真实方法。

正确解决方案

方案1:将Stub与模块导入移到测试生命周期钩子中

把Stub创建和Service1的导入移到before或beforeEach钩子内,确保Stub在测试即将运行时才创建,避免被其他测试的restore提前影响:

const sinon = require('sinon');
const { expect } = require('chai');

let Service1;
let func2stub;

describe('Service1 Tests', () => {
  before(() => {
    // 在钩子内加载Service2并创建Stub
    const { Service2 } = require('../Service2');
    func2stub = sinon.stub(Service2, 'func2');
    // 确保Service1加载时使用Stub后的Service2
    Service1 = require('../Service1').Service1;
  });

  // 可选:在after钩子手动恢复Stub,避免影响其他测试
  after(() => {
    func2stub.restore();
  });

  it('should return some result on calling func1', () => {
    func2stub.returns(true);
    expect(Service1.func1()).to.equal(true);
  });
});

方案2:使用Sinon沙箱(Sandbox)隔离Stub(推荐)

Sinon沙箱可隔离当前测试的Stub,沙箱的restore()只会恢复当前沙箱内创建的Stub,不会影响其他测试的全局状态,是更规范的做法:

const sinon = require('sinon');
const { expect } = require('chai');

describe('Service1 Tests', () => {
  let sandbox;
  let Service1;
  let func2stub;

  before(() => {
    sandbox = sinon.createSandbox();
    const { Service2 } = require('../Service2');
    func2stub = sandbox.stub(Service2, 'func2');
    Service1 = require('../Service1').Service1;
  });

  after(() => {
    // 仅恢复当前沙箱内的Stub,不影响其他测试
    sandbox.restore();
  });

  it('should return some result on calling func1', () => {
    func2stub.returns(true);
    expect(Service1.func1()).to.equal(true);
  });
});

方案3:清理模块缓存(按需使用)

如果需要彻底隔离不同测试间的模块实例,可以在钩子中清理require.cache,确保每次加载都是新的模块实例:

const sinon = require('sinon');
const { expect } = require('chai');

describe('Service1 Tests', () => {
  let Service1;
  let func2stub;

  before(() => {
    // 清理Service2和Service1的缓存
    delete require.cache[require.resolve('../Service2')];
    delete require.cache[require.resolve('../Service1')];
    
    const { Service2 } = require('../Service2');
    func2stub = sinon.stub(Service2, 'func2');
    Service1 = require('../Service1').Service1;
  });

  after(() => {
    func2stub.restore();
  });

  it('should return some result on calling func1', () => {
    func2stub.returns(true);
    expect(Service1.func1()).to.equal(true);
  });
});

最佳实践

  • 禁止在测试文件的顶级作用域创建Stub/Spy,避免测试间的交叉污染和被全局sinon.restore()影响。
  • 优先使用Sinon沙箱管理Stub,确保测试间的状态隔离。
  • 模块导入时机要晚于Stub创建,且放在测试生命周期钩子内,保证依赖的Stub生效。

内容的提问来源于stack exchange,提问作者gaurav5430

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.01 03:31:37