19

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.

Community
  • 1
  • 1
Joris Ooms
  • 11,880
  • 17
  • 67
  • 124
  • 2
    Oleg's answer looks good if you are a mockist TDDer, but most classical TDDers would [have no problem with hitting the database](http://stackoverflow.com/a/12526705/2646526). For an explanation on mocks, stubs, and mockist vs classical TDD see [Martin Fowler's article](http://martinfowler.com/articles/mocksArentStubs.html) on the subject. – heenenee Oct 25 '15 at 14:53
  • 2
    @heenenee At the end of the day tests are there to guarantee code quality, so there is no write or wrong as long as quality does not suffer. The drawbacks of basic unit tests hitting the DB are: (1) speed, (2) complexity for CI and individual project developers, (3) test side effects transferred via DB state, between individual tests or simultaneous test runs, which are difficult to solve, (4) fixing bug means extra effort for developers, in worst case external resource dependency. There is nothing wrong about all this, but really for integration tests only. I would clearly separate the two. – Oleg Sklyar Oct 25 '15 at 17:07
  • @heenenee Forgot to mention: thanks for the link to a very interesting article by Martin Fowler! – Oleg Sklyar Oct 25 '15 at 17:24
  • 3
    @Oleg I agree with all of that. Personally, I'm fine with not having unit tests for persistence-related code and having integration tests only. This stems from the "use real objects if possible" mindset of classical TDD. I just wanted to give that point of view and some background on that to the asker since he says he's new to stubbing/mocking and may not be aware of integration testing at all. – heenenee Oct 25 '15 at 19:05

2 Answers2

53

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

> xxx@0.1.0 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.

Oleg Sklyar
  • 9,834
  • 6
  • 39
  • 62
  • Thanks for the example and great explanation. I guess I'll implement it this way in my project and see how that works out. Would you recommend using an alternative to Mongoose? You mention Mongoose being unusable and say this way of mocking is 'no fun', so what would the alternative be? Not writing these tests would be one option, I suppose, haha. – Joris Ooms Oct 25 '15 at 15:41
  • 1
    @cabaret Mongoose is a generic ODM (object-document mapping) solution and the "generic" element did not work for me. I prefer clean class hierarchies over arguably a mix-mash of mongoose. So testing was not the primary reason, but it was a factor. So I ended up implementing a lesser generic ODM on top of the native driver tailored to my needs: I defined a clean API, tested it, and then made all my Classes (in O of ODM) use those functionalities. Was fun exercise, but not comprehensive enough to make it public. If you stick with mongoose you have essentially two options: to be continued – Oleg Sklyar Oct 25 '15 at 16:48
  • 2
    (1) use an approach like the one I outlined here, or something similar; or (2) run tests against a real DB doing essentially integration tests instead of unit tests. If this allows you to quickly get to the point of having a fully tested code that your team is happy to go along with (as all of them will need a DB etc), then take this pragmatic route. After all you care about the quality of your product, not whether a test can be called unit or must be called integration one. So do write tests in any case :) – Oleg Sklyar Oct 25 '15 at 16:51
  • I was looking for solution to test my `mongoose` stuff without connecting to the actual `db`. Is the solution above really `db` isolated? what `var conn = mongoose.createConnection();` is doing there? – angularrocks.com Feb 11 '16 at 23:45
8

If what you want is test static's and method's of certain Mongoose model, I would recommend you to use sinon and sinon-mongoose. (I guess it's compatible with chai)

This way, you won't need to connect to Mongo DB.

Following your example, suppose you have a static method findLast

//If you are using callbacks
PostSchema.static('findLast', function (n, callback) {
  this.find().limit(n).sort('-postDate').exec(callback);
});

//If you are using Promises
PostSchema.static('findLast', function (n) {
  this.find().limit(n).sort('-postDate').exec();
});

Then, to test this method

var Post = mongoose.model('Post');
// If you are using callbacks, use yields so your callback will be called
sinon.mock(Post)
  .expects('find')
  .chain('limit').withArgs(10)
  .chain('sort').withArgs('-postDate')
  .chain('exec')
  .yields(null, 'SUCCESS!');

Post.findLast(10, function (err, res) {
  assert(res, 'SUCCESS!');
});

// If you are using Promises, use 'resolves' (using sinon-as-promised npm) 
sinon.mock(Post)
  .expects('find')
  .chain('limit').withArgs(10)
  .chain('sort').withArgs('-postDate')
  .chain('exec')
  .resolves('SUCCESS!');

Post.findLast(10).then(function (res) {
  assert(res, 'SUCCESS!');
});

You can find working (and simple) examples on the sinon-mongoose repo.

Gon
  • 627
  • 7
  • 6