为何单个文件的sinon.restore()会还原其他文件的顶级Stub?
问题原因分析与解决方案
问题根源
- Node.js模块缓存机制:Node.js会缓存已加载的模块,所有
require('../Service2')调用都会返回同一个内存实例。Service1测试在顶级作用域创建的Stub,直接修改了这个缓存实例上的func2方法。 sinon.restore()的全局特性:sinon.restore()是全局方法,会恢复所有由Sinon创建的Stub、Spy和Mock,不受测试文件边界限制。如果另一个测试文件的afterEach钩子先执行并调用sinon.restore(),会把Service1测试中顶级作用域创建的Stub一并还原。- 测试执行顺序与顶级代码加载时机:测试运行时,所有测试文件的顶级代码会先于测试用例执行。若其他测试先运行,其
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
相关产品推荐
相关产品推荐

