使用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
相关产品推荐
相关产品推荐

