12

I would like to stub the save method available to Mongoose models. Here's a sample model:

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

I have some helper function that will call the save method.

/* 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);
  });
};

I would like to check that user.save is called inside my helper function using a unit test.

/* 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)
});

I discovered var saveStub = sinon.stub(mongoose.Model.prototype, 'save') from here.

All is fine unless I try to add something to my saveStub, e.g. with saveStub.yields(null). If I wanted to simulate an error being passed to the save callback with saveStub.yields('mock error'), I get this error:

TypeError: Attempted to wrap undefined property undefined as function

The stack trace is totally unhelpful.

The research I've done

I attempted to refactor my model to gain access to the underlying user model, as recommended here. That yielded the same error for me. Here was my code for that attempt:

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

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

I found that this solution didn't work for me at all. Maybe this is because I'm setting up my user model in a different way?

I've also tried Mockery following this guide and this one, but that was way more setup than I thought should be necessary, and made me question the value of spending the time to isolate the db.

My impression is that it all has to do with the mysterious way mongoose implements save. I've read something about it using npm hooks, which makes the save method a slippery thing to stub.

I've also heard of mockgoose, though I haven't attempted that solution yet. Anyone had success with that strategy? [EDIT: turns out mockgoose provides an in-memory database for ease of setup/teardown, but it does not solve the issue of stubbing.]

Any insight on how to resolve this issue would be very appreciated.

Community
  • 1
  • 1
Andrew Smith
  • 1,434
  • 13
  • 29

2 Answers2

10

Here's the final configuration I developed, which uses a combination of sinon and 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);
    });
  }
}
Andrew Smith
  • 1,434
  • 13
  • 29
7

Have you tried:

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

Also, where is the helper function getting called in the test? It looks like you define the function as the utils module, but call it as a method of a controller object. I'm assuming this has nothing to do with that error message, but it did make it harder to figure out when and where the stub was getting called.

  • Thanks for the response. Thanks for the catch. The difference in defining the function as the module and later calling it as a method was an error introduced when cleaning up the code and simplifying it for SO. I've corrected it now. – Andrew Smith Mar 05 '15 at 18:05
  • I did try using `sinon.stub(userModel.prototype, 'save')`. It does accurately capture the behavior on the `save` method, but it still throws the `TypeError` I described before. – Andrew Smith Mar 05 '15 at 18:32