Mocking/stubbing Mongoose model save method

cabaret picture cabaret · Oct 22, 2015 · Viewed 14k times · Source

Given a simple Mongoose model:

import mongoose, { Schema } from 'mongoose';

const PostSchema = Schema({
  title:    { type: String },
  postDate: { type: Date, default: Date.now }
}, { timestamps: true });

const Post = mongoose.model('Post', PostSchema);

export default Post;

I wish to test this model, but I'm hitting a few roadblocks.

My current spec looks something like this (some stuff omitted for brevity):

import mongoose from 'mongoose';
import { expect } from 'chai';
import { Post } from '../../app/models';

describe('Post', () => {
  beforeEach((done) => {
    mongoose.connect('mongodb://localhost/node-test');
    done();
  });

  describe('Given a valid post', () => {
    it('should create the post', (done) => {
      const post = new Post({
        title: 'My test post',
        postDate: Date.now()
      });

      post.save((err, doc) => {
        expect(doc.title).to.equal(post.title)
        expect(doc.postDate).to.equal(post.postDate);
        done();
      });
    });
  });
});

However, with this I'm hitting my database every time I run the test, which I would prefer to avoid.

I've tried using Mockgoose, but then my test won't run.

import mockgoose from 'mockgoose';
// in before or beforeEach
mockgoose(mongoose);

The test gets stuck and throws an error saying: Error: timeout of 2000ms exceeded. Ensure the done() callback is being called in this test. I've tried increasing the timeout to 20 seconds but that didn't solve anything.

Next, I threw away Mockgoose and tried using Sinon to stub the save call.

describe('Given a valid post', () => {
  it('should create the post', (done) => {
    const post = new Post({
      title: 'My test post',
      postDate: Date.now()
    });

    const stub = sinon.stub(post, 'save', function(cb) { cb(null) })
    post.save((err, post) => {
      expect(stub).to.have.been.called;
      done();
    });
  });
});

This test passes, but it somehow doesn't make much sense to me. I'm quite new to stubbing, mocking, what have you, ... and I'm not sure if this is the right way to go. I'm stubbing the save method on post, and then I'm asserting it to have been called, but I'm obviously calling it... Also, I can't seem to get to the arguments the non-stubbed Mongoose method would return. I would like to compare the post variable to something the save method returns, like in the very first test where I hit the database. I've tried a couple of methods but they all feel quite hackish. There must be a clean way, no?

Couple of questions:

  • Should I indeed avoid hitting the database like I've always read everywhere? My first example works fine and I could clear the database after each run. However, it doesn't really feel right to me.

  • How would I stub the save method from the Mongoose model and make sure it actually tests what I want to test: saving a new object to the db.

Answer

Oleg Sklyar picture Oleg Sklyar · Oct 25, 2015

The basics

In unit testing one should not hit the DB. I could think of one exception: hitting an in-memory DB, but even that lies already in the area of integration testing as you would only need the state saved in memory for complex processes (and thus not really units of functionality). So, yes no actual DB.

What you want to test in unit tests is that your business logic results in correct API calls at the interface between your application and the DB. You can and probably should assume that the DB API/driver developers have done a good job testing that everything below the API behaves as expected. However, you also want to cover in your tests how your business logic reacts to different valid API results such as successful saves, failures due to data consistency, failures due to connection issues etc.

This means that what you need and want to mock is everything that is below the DB driver interface. You would, however, need to model that behaviour so that your business logic can be tested for all outcomes of the DB calls.

Easier said than done because this means you need to have access to the API via the technology you use and you need to know the API.

The reality of mongoose

Sticking to the basics we want to mock the calls performed by the underlying 'driver' that mongoose uses. Assuming it is node-mongodb-native we need to mock out those calls. Understanding the full interplay between mongoose and the native driver is not easy, but it generally comes down to the methods in mongoose.Collection because the latter extends mongoldb.Collection and does not reimplement methods like insert. If we are able to control the behaviour of insert in this particular case, then we know we mocked out the DB access at the API level. You can trace it in the source of both projects, that Collection.insert is really the native driver method.

For your particular example I created a public Git repository with a complete package, but I will post all of the elements here in the answer.

The solution

Personally I find the "recommended" way of working with mongoose quite unusable: models are usually created in the modules where the corresponding schemas are defined, yet they already need a connection. For purposes of having multiple connections to talk to completely different mongodb databases in the same project and for testing purposes this makes life really hard. In fact, as soon as concerns are fully separated mongoose, at least to me, becomes nearly unusable.

So the first thing I create is the package description file, a module with a schema and a generic "model generator":

package.json

{
  "name": "xxx",
  "version": "0.1.0",
  "private": true,
  "main": "./src",
  "scripts": {
    "test" : "mocha --recursive"
  },
  "dependencies": {
    "mongoose": "*"
  },
  "devDependencies": {
    "mocha": "*",
    "chai": "*"
  }
}

src/post.js

var mongoose = require("mongoose");

var PostSchema = new mongoose.Schema({
    title: { type: String },
    postDate: { type: Date, default: Date.now }
}, {
    timestamps: true
});

module.exports = PostSchema;

src/index.js

var model = function(conn, schema, name) {
    var res = conn.models[name];
    return res || conn.model.bind(conn)(name, schema);
};

module.exports = {
    PostSchema: require("./post"),
    model: model
};

Such a model generator has its drawbacks: there are elements that may need to be attached to the model and it would make sense to place them in the same module where the schema is created. So finding a generic way to add those is a bit tricky. For example, a module could export post-actions to be automatically run when a model is generated for a given connection etc. (hacking).

Now let's mock the API. I'll keep it simple and will only mock what I need for the tests in question. It is essential that I would like to mock out the API in general, not individual methods of individual instances. The latter might be useful in some cases, or when nothing else helps, but I would need to have access to objects created inside of my business logic (unless injected or provided via some factory pattern), and this would mean modifying the main source. At the same time, mocking the API in one place has a drawback: it is a generic solution, which would probably implement successful execution. For testing error cases, mocking in instances in the tests themselves could be required, but then within your business logic you might not have direct access to the instance of e.g. post created deep inside.

So, let's have a look at the general case of mocking successful API call:

test/mock.js

var mongoose = require("mongoose");

// this method is propagated from node-mongodb-native
mongoose.Collection.prototype.insert = function(docs, options, callback) {
    // this is what the API would do if the save succeeds!
    callback(null, docs);
};

module.exports = mongoose;

Generally, as long as models are created after modifying mongoose, it is thinkable that the above mocks are done on per test basis to simulate any behaviour. Make sure to revert to the original behaviour, however, before every test!

Finally this is how our tests for all possible data saving operations could look like. Pay attention, these are not specific to our Post model and could be done for all other models with exactly the same mock in place.

test/test_model.js

// now we have mongoose with the mocked API
// but it is essential that our models are created AFTER 
// the API was mocked, not in the main source!
var mongoose = require("./mock"),
    assert = require("assert");

var underTest = require("../src");

describe("Post", function() {
    var Post;

    beforeEach(function(done) {
        var conn = mongoose.createConnection();
        Post = underTest.model(conn, underTest.PostSchema, "Post");
        done();
    });

    it("given valid data post.save returns saved document", function(done) {
        var post = new Post({
            title: 'My test post',
            postDate: Date.now()
        });
        post.save(function(err, doc) {
            assert.deepEqual(doc, post);
            done(err);
        });
    });

    it("given valid data Post.create returns saved documents", function(done) {
        var post = new Post({
            title: 'My test post',
            postDate: 876543
        });
        var posts = [ post ];
        Post.create(posts, function(err, docs) {
            try {
                assert.equal(1, docs.length);
                var doc = docs[0];
                assert.equal(post.title, doc.title);
                assert.equal(post.date, doc.date);
                assert.ok(doc._id);
                assert.ok(doc.createdAt);
                assert.ok(doc.updatedAt);
            } catch (ex) {
                err = ex;
            }
            done(err);
        });
    });

    it("Post.create filters out invalid data", function(done) {
        var post = new Post({
            foo: 'Some foo string',
            postDate: 876543
        });
        var posts = [ post ];
        Post.create(posts, function(err, docs) {
            try {
                assert.equal(1, docs.length);
                var doc = docs[0];
                assert.equal(undefined, doc.title);
                assert.equal(undefined, doc.foo);
                assert.equal(post.date, doc.date);
                assert.ok(doc._id);
                assert.ok(doc.createdAt);
                assert.ok(doc.updatedAt);
            } catch (ex) {
                err = ex;
            }
            done(err);
        });
    });

});

It is essential to note that we are still testing the very low level functionality, but we can use this same approach for testing any business logic that uses Post.create or post.save internally.

The very final bit, let's run the tests:

~/source/web/xxx $ npm test

> [email protected] test /Users/osklyar/source/web/xxx
> mocha --recursive

Post
  ✓ given valid data post.save returns saved document
  ✓ given valid data Post.create returns saved documents
  ✓ Post.create filters out invalid data

3 passing (52ms)

I must say, this is no fun to do it that way. But this way it is really pure unit-testing of the business logic without any in-memory or real DBs and fairly generic.