Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Stubbing the mongoose save method on a model

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.

like image 407
Andrew Smith Avatar asked Mar 03 '15 05:03

Andrew Smith


1 Answers

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);
    });
  }
}
like image 81
Andrew Smith Avatar answered Oct 17 '22 23:10

Andrew Smith