Mongoose models and unit tests: The definitive guide

Tags:

Mongoose is a great tool, as it helps you build Node apps which use MongoDB more easily. Instead of having to sprinkle the model-related logic all over the place, it’s easy to define the functionality as Mongoose models. Querying MongoDB for data also becomes quick and easy – and if you ever need some custom querying logic, that can be hidden away into the model as well.

But how do you test all that logic? When trying to test them, Mongoose models can be a bit daunting at first. Ideally, we’d like to test them without having to connect to a database – after all, a connection would make the tests slow and difficult to set up, because we need to manage database state. But how can we do that?

In fact, Mongoose related questions are some of the most common library-specific ones I get from my readers.

So, let’s find out how we can test Mongoose models. We’ll go in depth with examples for the most common testing situations you’d encounter with Mongoose.

Getting started

Testing Mongoose models can look pretty complex at first – we have schemas, models, validators, finders, statics…

Not only that, but there are two parts that we need to understand:

  1. Testing Mongoose model logic itself – such as validation
  2. Testing code that uses our models – such as using finders or querying MongoDB in some other ways

So it’s quite understandable why this can pose a challenge at first. But once you master the testing techniques I’ll show you, it’ll become very simple. The best part? The techniques used for Mongoose models are the same techniques you’d use for testing other code.

The tools we’re going to use are Mocha for running tests, Chai for assertions, and lastly, Sinon for creating stubs where necessary.

We can set them up in our project like this:

npm install -g mocha

npm install --save sinon chai

Part 1: Testing the model

We’re going to start by looking at how to test different parts of model objects.

Testing model validations

One of the most important aspects of a good model is validation. You don’t want invalid data to go into your database, especially as MongoDB itself doesn’t really care – Oh, this data looks weird? Just chuck it in there!

Mongoose normally validates objects when you call save(), before the data is sent to MongoDB. Instead of this, we can write our tests using validate(), which doesn’t require a database connection.

For example, let’s say we have the following schema and model:

var mongoose = require('mongoose');
 
var memeSchema = new mongoose.Schema({
    name: { type: String }
});
 
module.exports = mongoose.model('Meme', memeSchema);

Now of course we don’t want to allow saving this without a name. How can we write a test for that?

var expect = require('chai').expect;
 
var Meme = require('../src/Meme');
 
describe('meme', function() {
    it('should be invalid if name is empty', function(done) {
        var m = new Meme();
 
        m.validate(function(err) {
            expect(err.errors.name).to.exist;
            done();
        });
    });
});

If you run this test, you’ll quickly find out that the schema is missing the required attribute. If we include it in the schema, the tests will pass:

var memeSchema = new mongoose.Schema({
    name: { type: String, required: true }
});

OK, so that’s quite simple. What about other types of validations?

You know what, that’s it! You can use this exact same pattern to test any kind of validation logic!

  1. Create a model with data that should put the validation into the state we want
  2. Run validate with a callback
  3. In the callback, do an assertion for the error property

Let’s look at another slightly more advanced example.

We want to only allow reposts if it’s a dank meme:

var memeSchema = new mongoose.Schema({
    name: { type: String, required: true },
    dank: { type: Boolean },
    repost: {
        type: Boolean,
        validate: function(v) {
            return v === true && this.dank === true;
        }
    }
});

The validator function for repost only passes when dank is also set to true.

Now let’s write tests to check for this, so you’ll see how we can test this quite different validator by following the exact same steps as before.

it('should have validation error for repost if not dank', function(done) {
    //1. set up the model in a way the validation should fail
    var m = new Meme({ repost: true });
 
    //2. run validate
    m.validate(function(err) {
        //3. check for the error property we need
        expect(err.errors.repost).to.exist;
        done();
    });
});
 
it('should be valid repost when dank', function(done) {
    //1. set up the model in a way the validation should succeed
    var m = new Meme({ repost: true, dank: true });
 
    //2. run validate 
    m.validate(function(err) {
        //3. check for the error property that shouldn't exist now
        expect(err.errors.repost).to.not.exist;
        done();
    });
});

This time we’ve done the test for both cases: Failure and success. As this validation is more complex, it made sense to check for both conditions.

As you can see, we’ve followed the same steps I laid out to write these two different kinds of validation tests as well.

Testing model instance methods

You’d typically have two kinds of instance methods on your models:

  1. Instance methods which do not access the database
  2. Instance methods which access the database

When testing #1, the test is simple: Just call the function with any parameters it expects, and check the value it returns / callback response.

#2 can be a bit more challenging. Let’s say we have a function which is used to check if a repost exists for a given meme:

memeSchema.methods.checkForReposts = function(cb) {
    this.model('Meme').findOne({
        name: this.name,
        repost: true
    }, function(err, val) {
        cb(!!val);
    });
};

This method simply queries MongoDB for any document with the same name, with repost set to true. Let’s see how we’d go about testing this.

First, to ensure the function is doing the correct query, we can write a test like this:

it('should check for reposts with same name', sinon.test(function() {
    this.stub(Meme, 'findOne');
    var expectedName = 'This name should be used in the check';
    var m = new Meme({ name: expectedName });
 
    m.checkForReposts(function() { });
 
    sinon.assert.calledWith(Meme.findOne, {
        name: expectedName,
        repost: true
    });
}));

We start by stubbing Meme.findOne. This is the function that would get called, so we stub it out so it doesn’t do any database access. Stubbing it also allows us to use Sinon to check whether it was called with the correct parameters.

Then, we set up a variable expectedName to contain the name we want to check for. We create a new Meme object with the name, and call checkForReposts.

Finally, we use sinon.assert.calledWith to check the stubbed finder was called correctly. Note that by saving the expected name into a variable, we saved having to retype it here, plus it makes the code a bit easier to follow, as you can see what the expected values are.

We should also have another test for this to confirm the result from findOne is handled correctly. When a repost exists, the callback function should be called with true as its parameter:

it('should call back with true when repost exists', sinon.test(function(done) {
    var repostObject = { name: 'foo' };
    this.stub(Meme, 'findOne').yields(null, repostObject);
    var m = new Meme({ name: 'some name' });
 
    m.checkForReposts(function(hasReposts) {
        expect(hasReposts).to.be.true;
        done();
    });
}));

Similar to before, we create a stub for Meme.findOne. In this case, we have it yield the values null and repostObject. The yields function on a stub makes it automatically call any callback function with a certain set of parameters – in this case, we’re passing null to signify no error, and the repostObject to act as a found Mongoose model.

This time we use a callback in checkForReposts to do the assertion, as we want to ensure it was called with the correct value.

Testing static functions

Testing static Mongoose model functions works exactly the same way as testing instance methods. The difference is you don’t create model instances in your test.

  1. Stub out any database accessors
  2. If testing any logic other than DB queries, you can set up the stubs to return values
  3. sinon.assert.calledWith can be used to see the correct DB query was made

Part 2: Testing code which uses Mongoose models

Now that we’ve covered testing the model itself, let’s take a look at how to test code using those models.

In most cases, this part is quite straightforward. We can use stubs to do most of the lifting for the tests.

Either way, let’s take a look at some examples of such code and how we’d go about testing it.

Testing a function which queries for data

In an app the most common action you’d take with models is querying the database. It could be a service, a helper function, or maybe just an Express route where the query happens.

Let’s say we have the following function:

var Meme = require('./Meme');
 
module.exports.allMemes = function(req, res) {
    Meme.find({ repost: req.params.reposts }, function(err, memes) {
        res.send(memes);
    });
};

This could be a route used with Express. It loads all memes and sends them out as JSON, with an optional reposts flag from the parameters.

We can test this function easily by stubbing out Meme.find:

var expect = require('chai').expect;
var sinon = require('sinon');
 
var routes = require('../src/routes');
var Meme = require('../src/Meme');
 
describe('routes', function() {
    beforeEach(function() {
        sinon.stub(Meme, 'find');
    });
 
 
    afterEach(function() {
        Meme.find.restore();
    });
 
    it('should send all memes', function() {
        var a = { name: 'a' };
        var b = { name: 'b' };
        var expectedModels = [a, b];
        Meme.find.yields(null, expectedModels);
        var req = { params: { } };
        var res = {
            send: sinon.stub()
        };
 
        routes.allMemes(req, res);
 
        sinon.assert.calledWith(res.send, expectedModels);
    });
});

In this case, we can use beforeEach and afterEach to automatically stub and restore the finder function. It’s convenient to use these hooks in cases where we need the same stub in multiple tests.

Similar to our earlier tests, in this one we’re setting up some expeced data – in this case, the expected list of models the find function should return. We also set up the stub to yield the result.

The req and res parameters for the route are set up here as well. For res, we set a stub as the send function, so that we can assert against it later.

After calling the route, we again use sinon.assert.calledWith to verify the correct behavior.

We can also have a test to verify the behavior with the reposts flag:

it('should query for non-reposts if set as request parameter', function() {
    Meme.find.yields(null, []);
    var req = {
        params: {
            reposts: true
        }
    };
    var res = { send: sinon.stub() };
 
    routes.allMemes(req, res);
 
    sinon.assert.calledWith(Meme.find, { repost: true });
});

You can see we use a very similar technique here as before – set up the stub, set up the data, call the function, assert.

We can use this exact same pattern to test a lot of things. In fact, the same pattern even repeats in our model specific tests from before.

Bonus part: Dealing with test data

(Thanks to Valeri Karpov for suggesting including this)

When writing tests like these, you’ll often reuse the same types of test data. For example, when testing different routes or modules using your models, you need to create fake data for your tests.

Like you saw above, you can usually start by just defining your test data inline within your test. But with more and more tests, chances are you’ll end up repeating the same test data. This can be problematic especially if your tests need the data to look correct – let’s say some code is using one of the properties of your model.

To avoid having to copypaste test data across your tests, you can create helper functions which create the data for you.

For example, we can have a file test/factories.js where we put this kind of code. The reason I’m calling it factories is a function which creates an object of some type is often called a factory.

module.exports.validMeme = function() {
  return {
    name: 'Some name here',
    dank: false,
    repost: false
  };
};
 
module.exports.repostMeme = function() {
  return {
    name: 'Some name here',
    dank: false,
    repost: true
  };
};

We could rewrite the earlier test where we had some test data to look like this:

var factories = require('./factories');
 
it('should send all memes', function() {
  var a = factories.validMeme();
  var b = factories.validMeme();
  var expectedModels = [a, b];
  Meme.find.yields(null, expectedModels);
  var req = { params: { } };
  var res = {
    send: sinon.stub()
  };
 
  routes.allMemes(req, res);
 
  sinon.assert.calledWith(res.send, expectedModels);
});

The benefits of this approach is it can reduce the amount of code you need in your tests, and make maintaining them easier. For example, if we add a new field to one of our models, we only need to update the factories, instead of having to go through potentially dozens of tests.

Conclusion

Unit testing apps which use libraries like Mongoose can seem complicated at first. But if you learn the basics and apply them, you can notice the same testing patterns keep repeating.

When testing models or code using them, the key point is to identify what needs to be stubbed. Usually if it would talk to the database, that’s what needs to go. Once you do that, it’s just a matter of setting the stub to do something, and there you go.

A project with the example code and everything set up is available on Github.

Interested in learning more about how you can use Sinon.js to make testing other kinds of code really easy? Go and grab my free Sinon.js in the Real-world guide – it has all my best Sinon.js content put together in one place!