如何在Express中间件中存根依赖?(含express-validator等工具)
问题:Proxyquire存根Sequelize模型的findOne方法未生效
可运行的基础示例
先看一个正常工作的简单场景:我有两个模块,moduleA 依赖 moduleB
moduleA.js
const ModuleB = require('./moduleB'); function functionA() { return 20 + ModuleB.functionB(); }; module.exports = { functionA };
moduleB.js
const functionB = () => { return 10; } module.exports = { functionB }
测试文件用proxyquire存根moduleB的functionB:
const sinon = require('sinon'); const proxyquire = require('proxyquire'); describe('Unit Tests', function() { it('should work', () => { const mockedFn = sinon.stub(); mockedFn.returns(30); const copyModuleA = proxyquire('./moduleA', { './moduleB': { functionB: mockedFn } }); console.log(copyModuleA.functionA()); }) });
运行后输出50(存根后的functionB返回30加上functionA的20)
实际项目中的问题
将上述逻辑迁移到实际项目时遇到了问题:
- 示例中的
moduleA对应项目里的validation.js - 依赖的
moduleB是Sequelize模型Person,需要存根它的findOne方法
validation.js导出module.exports = { validateLogin };,validateLogin会调用内部的validate函数,最终在自定义验证规则中调用Person.findOne()。
我按照基础示例的思路编写了测试代码,但存根并未生效:
测试代码
let validationModule; describe('Unit Tests', () => { before(() => { const stubbedFindOne = sinon.stub(); stubbedFindOne.resolves(); validationModule = proxyquire('../../utils/validation', { '../models/Person': { findOne: stubbedFindOne } }); }) it.only('should return 422 if custom email validation fails', async() => { const wrongEmailReq = { body: {email: 'nik@hotmail.com'} }; const res = { statusCode: 500, status: (code) => {this.statusCode = code; return this}, }; const validationFn = validationModule.validateLogin(); const wrongEmail = await validationFn(wrongEmailReq, res, ()=>{}); expect(wrongEmail.errors[0].msg).to.be.equal('Custom Authorisation Error'); return; })
validation.js代码
const Person = require('../models/Person'); // parallel processing const validate = validations => { return async (req, res, next) => { await Promise.all(validations.map(validation => validation.run(req))); const errors = validationResult(req); if (errors.isEmpty()) { return next(); } const error = new Error(); error.message = process.env.NODE_ENV === 'development'? 'Validation Failed':'Error'; error.statusCode = !errors.isEmpty()? 422:500; error.errors = errors.array({onlyFirstError: true}); next(error); return error; }; }; const validateLogin = () => { const validations = [ body('email') .isString() // 省略其他验证规则 .custom(async (value, {req}) => { try{ const person = await Person.findOne({ where: { email: value } }); if(!person) return Promise.reject('Custom Authorisation Error'); } catch(err) { throw err; } }) .trim(), ]; return validate(validations); } module.exports = { validateLogin };
问题原因与解决方法
核心原因
- Sequelize模型的导出格式:Sequelize模型是类,
findOne是类的静态方法,而非普通对象的属性。直接存根{ findOne: stub }的方式不符合模型的导出结构,无法被正确识别。 - 模块缓存问题:如果
validation.js或Person模型在测试前已被其他模块加载,Node.js的模块缓存会导致proxyquire替换的版本不生效。
解决步骤
1. 正确模拟Sequelize模型的静态方法
调整存根逻辑,模拟符合Sequelize模型结构的对象,让findOne作为静态方法存在:
before(() => { // 创建模拟的Person模型,包含静态方法findOne const mockedPerson = {}; // 让findOne返回null,触发自定义错误逻辑 mockedPerson.findOne = sinon.stub().resolves(null); validationModule = proxyquire('../../utils/validation', { '../models/Person': mockedPerson }); })
2. 清除模块缓存
在加载proxyquire前,清除目标模块的缓存,避免旧版本干扰:
before(() => { // 清除validation和Person模块的缓存 delete require.cache[require.resolve('../../utils/validation')]; delete require.cache[require.resolve('../models/Person')]; const mockedPerson = {}; mockedPerson.findOne = sinon.stub().resolves(null); validationModule = proxyquire('../../utils/validation', { '../models/Person': mockedPerson }); })
3. 修正后的完整测试代码
let validationModule; describe('Unit Tests', () => { before(() => { // 清除模块缓存 delete require.cache[require.resolve('../../utils/validation')]; delete require.cache[require.resolve('../models/Person')]; const stubbedFindOne = sinon.stub(); stubbedFindOne.resolves(null); // 返回null触发自定义错误 validationModule = proxyquire('../../utils/validation', { '../models/Person': { findOne: stubbedFindOne } }); }) it.only('should return 422 if custom email validation fails', async() => { const wrongEmailReq = { body: {email: 'nik@hotmail.com'} }; const res = { statusCode: 500, status: (code) => { this.statusCode = code; return this; }, }; const validationFn = validationModule.validateLogin(); const wrongEmail = await validationFn(wrongEmailReq, res, ()=>{}); expect(wrongEmail.errors[0].msg).to.be.equal('Custom Authorisation Error'); }) })
额外注意事项
- 若使用Sequelize类式模型(
class Person extends Model),可以直接继承Model并存根静态方法:const { Model } = require('sequelize'); class MockPerson extends Model {} sinon.stub(MockPerson, 'findOne').resolves(null); validationModule = proxyquire('../../utils/validation', { '../models/Person': MockPerson }); - 确保proxyquire中指定的依赖路径,与
validation.js中require('../models/Person')的路径完全一致,否则无法正确替换依赖。
内容的提问来源于stack exchange,提问作者NickW
相关产品推荐
相关产品推荐

