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

如何在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
};

问题原因与解决方法

核心原因

  1. Sequelize模型的导出格式:Sequelize模型是类,findOne是类的静态方法,而非普通对象的属性。直接存根{ findOne: stub }的方式不符合模型的导出结构,无法被正确识别。
  2. 模块缓存问题:如果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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.03 12:10:55