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

使用Sinon对函数内require模块Stub的可行性及测试咨询

关于函数内部require场景下的单元测试Stub方案

嘿,这个场景我太熟悉了——函数内部的require确实会给单元测试添不少麻烦,咱们一步步来捋清楚解决方案:

当前结构的核心问题

你现在的代码里,aControllerFunction每次调用都会执行require('../services/aService'),这意味着每次调用都会重新加载模块并创建全新的aService实例。常规Stub工具(比如Sinon)依赖模块缓存来替换导出内容,这种动态加载的方式会直接绕开缓存,导致Stub根本无法生效。

方案一:不调整结构,用工具强行Stub(可行但偏hack)

如果暂时不想修改代码结构,可以用proxyquire或者rewire这类专门处理模块依赖的工具,拦截内部的require调用:

测试aControllerFunction时Stub aService

用proxyquire替换aController.js里对aService的依赖:

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

describe('aControllerFunction', () => {
  it('should call aServiceFunction with correct params', () => {
    // 创建Stub的aService
    const stubbedAService = {
      aServiceFunction: sinon.stub()
    };
    // 用proxyquire加载aController,替换内部的aService依赖
    const aController = proxyquire('./aController', {
      '../services/aService': () => stubbedAService
    })(/* 传入mock的sqlConnection */);
    
    // 模拟req对象
    const req = { a: 'testA', b: 'testB' };
    // 调用控制器函数
    aController.aControllerFunction(req, {}, () => {});
    
    // 验证Stub被正确调用
    sinon.assert.calledWith(stubbedAService.aServiceFunction, 'testA', 'testB');
  });
});

测试aServiceFunction时Stub sqlConnection和models

这部分其实相对简单,因为aService的导出函数本身就接受sqlConnection和models作为参数,直接传入带Stub的对象即可:

const sinon = require('sinon');
const aServiceFactory = require('./aService');

describe('aServiceFunction', () => {
  it('should call update and queryAsync correctly', () => {
    // 创建Stub的sqlConnection和models
    const stubbedSqlConn = {
      queryAsync: sinon.stub().resolves()
    };
    const stubbedModels = {
      aModel: {
        update: sinon.stub()
      }
    };
    // 创建aService实例
    const aService = aServiceFactory(stubbedSqlConn, stubbedModels);
    
    // 调用服务函数
    aService.aServiceFunction('testA', 'testB');
    
    // 验证Stub调用
    sinon.assert.calledWith(stubbedModels.aModel.update, 'testA');
    sinon.assert.calledWith(stubbedSqlConn.queryAsync, sinon.match(/UPDATE.*testA/));
  });
});

方案二:调整结构(更推荐,长期维护更友好)

上面的hack方式虽然能解决问题,但代码可读性和维护性都不高。更优雅的做法是依赖注入,把aService的初始化移到函数外部:

调整aController.js的结构

把aService的创建放到模块导出的函数里,避免每次调用控制器函数都重新require:

module.exports = (sqlConnection, models) => {
  // 只初始化一次aService
  const aService = require('../services/aService')(sqlConnection, models);
  
  return {
    aControllerFunction
  };

  function aControllerFunction(req, res, next) {
    // 直接用已初始化的aService
    aService.aServiceFunction(req.a, req.b);
  }
};

这样调整后,测试控制器就简单多了——你可以直接传入Stub的aService,或者在创建控制器时传入Stub的sqlConnection和models间接控制aService的行为,完全不用纠结内部的require逻辑。

总结

  • 当前结构可以通过工具实现Stub,但属于临时解决方案,代码不够优雅;
  • 更推荐调整为依赖注入模式,既让代码的依赖关系更清晰,也让单元测试变得简单直观。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.09 14:23:11