Mongoose models and unit tests: The definitive guide

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.

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.

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 failvar 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 succeedvar 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();});});

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:

Instance methods which do not access the database

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:

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:

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.

Stub out any database accessors

If testing any logic other than DB queries, you can set up the stubs to return values

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.

Bonus part: Dealing with test data

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.

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!