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

如何使用Sinon对单例对象进行Stub/Spy以编写单元测试

如何用Sinon Spy/Stub单例对象进行单元测试

我完全懂你的痛点——单例因为其“唯一实例”的特性,确实比普通模块方法的Stub/Spy要费点心思,但用Sinon绝对可以搞定,不用只能靠集成测试!下面分两种最常见的单例导出场景来讲解:

场景1:单例实例直接作为模块导出

如果你的单例是直接导出实例(比如下面这种写法):

// singleton.js
class DatabaseClient {
  query(sql) { /* 真实数据库操作 */ }
}

// 直接导出实例,形成单例
module.exports = new DatabaseClient();

这种情况最简单,你只需要require这个单例实例,然后直接Stub它的方法就行:

const sinon = require('sinon');
const dbClient = require('./singleton'); // 拿到单例实例
const service = require('./your-service-that-uses-db'); // 依赖单例的业务模块

describe('Service', () => {
  afterEach(() => {
    // 每个测试后恢复原方法,避免污染其他用例
    sinon.restore();
  });

  it('should call dbClient.query with correct SQL', () => {
    // 1. Stub单例的query方法
    const queryStub = sinon.stub(dbClient, 'query').resolves({ rows: [] });

    // 2. 执行要测试的业务方法
    service.fetchUsers();

    // 3. 验证方法被调用,且参数正确
    sinon.assert.calledOnce(queryStub);
    sinon.assert.calledWith(queryStub, 'SELECT * FROM users');
  });
});

原理是Node.js的模块缓存机制:你require的单例实例和业务模块里require的是同一个,所以Stub会直接生效。

场景2:通过工厂函数获取单例

如果你的单例是通过getInstance这类工厂函数暴露的(比如下面的写法):

// singleton.js
class CacheService {
  get(key) { /* 真实缓存操作 */ }
}

let instance;
module.exports = {
  getInstance: () => {
    if (!instance) {
      instance = new CacheService();
    }
    return instance;
  }
};

这里有两种处理方式:

方式A:获取真实实例后Stub方法

先调用getInstance拿到实例,再Stub实例上的方法:

const sinon = require('sinon');
const cacheModule = require('./singleton');
const cacheService = cacheModule.getInstance();
const userRepo = require('./user-repo');

describe('UserRepo', () => {
  afterEach(() => sinon.restore());

  it('should fetch user from cache', () => {
    const getStub = sinon.stub(cacheService, 'get').returns({ id: 1, name: 'Alice' });

    userRepo.getUserById(1);

    sinon.assert.calledOnce(getStub);
    sinon.assert.calledWith(getStub, 'user:1');
  });
});

方式B:Stub工厂函数返回Mock实例

如果你想完全隔离真实的单例实例(比如避免初始化时的副作用),可以直接StubgetInstance让它返回一个你自己创建的Mock对象:

const sinon = require('sinon');
const cacheModule = require('./singleton');
const userRepo = require('./user-repo');

describe('UserRepo', () => {
  afterEach(() => sinon.restore());

  it('should fetch user from cache', () => {
    // 创建一个带Spy的Mock实例
    const mockCache = { get: sinon.spy(() => ({ id: 1, name: 'Alice' })) };
    // Stub getInstance返回这个Mock
    sinon.stub(cacheModule, 'getInstance').returns(mockCache);

    userRepo.getUserById(1);

    // 验证Mock上的方法被调用
    sinon.assert.calledOnce(mockCache.get);
    sinon.assert.calledWith(mockCache.get, 'user:1');
  });
});

进阶:用proxyquire绕过模块缓存

如果你的单例初始化有副作用,或者你需要在每个测试用例里重新初始化单例,可以用proxyquire来替换依赖的单例模块:

const sinon = require('sinon');
const proxyquire = require('proxyquire');

describe('UserRepo', () => {
  it('should fetch user from cache', () => {
    const mockCache = { get: sinon.spy(() => ({ id: 1, name: 'Alice' })) };
    // 用proxyquire加载业务模块,替换掉它依赖的singleton模块
    const userRepo = proxyquire('./user-repo', {
      './singleton': mockCache
    });

    userRepo.getUserById(1);

    sinon.assert.calledOnce(mockCache.get);
  });
});

关键注意事项

  • 测试隔离:每个测试用例后一定要恢复Stub(用sinon.restore()或者手动调用stub.restore()),避免Stub污染其他测试。
  • 模块缓存:Node.js会缓存模块导出,所以如果你的测试需要重新初始化单例,一定要用proxyquire或者清除模块缓存(不推荐后者)。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.19 07:15:11