在模型上存入 mongoose save 方法

Stubbing the mongoose save method on a model

我想存根可用于 Mongoose 模型的 save 方法。这是一个示例模型:

/* model.js */
var mongoose = require('mongoose');
var userSchema = mongoose.Schema({
  username: {
    type: String,
    required: true
  }
});
var User = mongoose.model('User', userSchema);
module.exports = User;

我有一些辅助函数可以调用 save 方法。

/* utils.js */
var User = require('./model');
module.exports = function(req, res) {
  var username = req.body.username;
  var user = new User({ username: username });
  user.save(function(err) {
    if (err) return res.end();
    return res.sendStatus(201);
  });
};

我想使用单元测试检查 user.save 是否在我的辅助函数中被调用。

/* test.js */
var mongoose = require('mongoose');
var createUser = require('./utils');
var userModel = require('./model');

it('should do what...', function(done) {
  var req = { username: 'Andrew' };
  var res = { sendStatus: sinon.stub() };
  var saveStub = sinon.stub(mongoose.Model.prototype, 'save');
  saveStub.yields(null);

  createUser(req, res);

  // because `save` is asynchronous, it has proven necessary to place the
  // expectations inside a setTimeout to run in the next turn of the event loop
  setTimeout(function() {
    expect(saveStub.called).to.equal(true);
    expect(res.sendStatus.called).to.equal(true);
    done();
  }, 0)
});

我从 here 中发现了 var saveStub = sinon.stub(mongoose.Model.prototype, 'save')

一切都很好,除非我尝试向我的 saveStub 添加一些东西,例如saveStub.yields(null)。如果我想用 saveStub.yields('mock error') 模拟传递给 save 回调的错误,我会收到此错误:

TypeError: Attempted to wrap undefined property undefined as function

堆栈跟踪完全没有帮助。

我做过的研究

我尝试按照建议重构我的模型以访问底层用户模型 here。这对我产生了同样的错误。这是我的尝试代码:

/* in model.js... */
var UserSchema = mongoose.model('User');
User._model = new UserSchema();

/* in test.js... */
var saveStub = sinon.stub(userModel._model, 'save');

我发现 this solution 根本不适合我。也许这是因为我以不同的方式设置了我的用户模型?

我也尝试过 this guide and this one 之后的 Mockery,但是这比我认为必要的设置要多得多,这让我质疑花时间隔离数据库的价值。

我的印象是,这一切都与 mongoose 实现的神秘方式有关 save。我已经使用 npm hooks 阅读了一些关于它的内容,这使得 save 方法很难存根。

我也听说过 mockgoose,但我还没有尝试过该解决方案。有人成功地实施了该策略吗? [编辑:原来 mockgoose 提供了一个内存数据库以方便 setup/teardown,但它没有解决存根问题。]

任何有关如何解决此问题的见解都将不胜感激。

你试过了吗:

sinon.stub(userModel.prototype, 'save')

另外,测试中辅助函数在哪里被调用?看起来您将该函数定义为 utils 模块,但将其作为控制器对象的方法调用。我假设这与该错误消息无关,但它确实让我们更难确定调用存根的时间和地点。

这是我开发的最终配置,它使用了 sinon 和 mockery 的组合:

// Dependencies
var expect = require('chai').expect;
var sinon = require('sinon');
var mockery = require('mockery');
var reloadStub = require('../../../spec/utils/reloadStub');

describe('UNIT: userController.js', function() {

  var reportErrorStub;
  var controller;
  var userModel;

  before(function() {
    // mock the error reporter
    mockery.enable({
      warnOnReplace: false,
      warnOnUnregistered: false,
      useCleanCache: true
    });

    // load controller and model
    controller = require('./userController');
    userModel = require('./userModel');
  });

  after(function() {
    // disable mock after tests complete
    mockery.disable();
  });

  describe('#createUser', function() {
    var req;
    var res;
    var status;
    var end;
    var json;

    // Stub `#save` for all these tests
    before(function() {
      sinon.stub(userModel.prototype, 'save');
    });

    // Stub out req and res
    beforeEach(function() {
      req = {
        body: {
          username: 'Andrew',
          userID: 1
        }
      };

      status = sinon.stub();
      end = sinon.stub();
      json = sinon.stub();

      res = { status: status.returns({ end: end, json: json }) };
    });

    // Reset call count after each test
    afterEach(function() {
      userModel.prototype.save.reset();
    });

    // Restore after all tests finish
    after(function() {
      userModel.prototype.save.restore();
    });

    it('should call `User.save`', function(done) {
      controller.createUser(req, res);
      /**
       * Since Mongoose's `new` is asynchronous, run our expectations on the
       * next cycle of the event loop.
       */
      setTimeout(function() {
        expect(userModel.prototype.save.callCount).to.equal(1);
        done();
      }, 0);
    });
  }
}