How to test your MongoDB models under Node & Express

Models are the base units of every application. When there is a bug in them they might affect the functionality of the entire app. As a result you will lose users and revenue.

Therefore you should test your models, but this is not as trivial as it sounds.

First, when testing a model you don’t know what was the state of the database before you run the test. When you don’t know what the initial state is, you don’t know what to expect as a result from the test.

Second, even if you know the state for one particular test, you want to have a predictable state of the database for the next test as well, and for every other too.

Last, while you develop you are probably testing manually, and you probably have some useful data, which you want to keep.

You don’t really want that your testis mess up your current database state, and transform it into something that you might not be able to work with.

Imagine how

  • Before each test your database goes into a clean state
  • You always know what to expect and the results of your tests is predictable
  • While your tests interact with the database, they don’t affect the data that you use while developing
  • All this happens automatically and you can just safely create your tests

This is achievable. Let’s begin with the tools that we are going to use

Tools

There are many options to choose from when it comes to testing in Node & Express. There are many test runners, test frameworks, assertion frameworks and so on.

I won’t show you every one of them. Instead let’s focus on how to organize your test.

We are going to use mocha to write your tests. It is a descriptive and easy to read and use testing framework. You will love it.

We are also going to use should.js, a readable assertion library which makes checking every result a breeze.

To install them run

$ npm install mocha should --save-dev

Database

For this example we are going to use MongoDB. However, you can apply what you learn with any other database. I’ve already used all of this with Redis, CouchDB, MySQL and SQLite.

Database utilities

Usually database drivers only provide a way to connect and to do basic database operations, but little useful stuff for testing.

Moreover, in your files there are multiple places where you need to connect to your database. Every model for example is such a place.

You don’t want to repeat your code everywhere. You need some database utilities to help you with that.

We are going to use the following two NPM modules to help us

$ npm install mongodb async --save

You need the mongodb to connect to the database and work with it. The async model solves completely the infamous “callback hell”.

Let’s build a database module, like this one.

var MongoClient = require('mongodb').MongoClient
  , async = require('async')

var state = {
  db: null,
  mode: null,
}

// In the real world it will be better if the production uri comes

// from an environment variable, instead of being hard coded.

var PRODUCTION_URI = 'mongodb://127.0.0.1:27017/production'
  , TEST_URI = 'mongodb://127.0.0.1:27017/test'

exports.MODE_TEST = 'mode_test'
exports.MODE_PRODUCTION = 'mode_production'

exports.connect = function(mode, done) {
  if (state.db) return done()

  var uri = mode === exports.MODE_TEST ? TEST_URI : PRODUCTION_URI

  MongoClient.connect(uri, function(err, db) {
    if (err) return done(err)
    state.db = db
    state.mode = mode
    done()
  })
}

exports.getDB = function() {
  return state.db
}

exports.drop = function(done) {
  if (!state.db) return done()
  // This is faster then dropping the database

  state.db.collections(function(err, collections) {
    async.each(collections, function(collection, cb) {
      if (collection.collectionName.indexOf('system') === 0) {
        return cb()
      }
      collection.remove(cb)
    }, done)
  })
}

exports.fixtures = function(data, done) {
  var db = state.db
  if (!db) {
    return done(new Error('Missing database connection.'))
  }

  var names = Object.keys(data.collections)
  async.each(name, function(name, cb) {
    db.createCollection(name, function(err, collection) {
      if (err) return cb(err)
      collection.insert(data.collections[name], cb)
    })
  }, done)
}

This module contains several very useful methods.

  • connect - to connect to either the production or the test database
  • getDB - to get an active database connection
  • drop - to clear all collections in the database
  • fixtures - load data from a JSON structure into the database

You can already see that drop and fixtures will help you with setting up the right state and that connect has a testing mode which we can use to avoid changing your database while developing.

Example

Let’s imagine that you have the following app structure

app.js
db.js
controllers/
models/
  comment.js
tests/
  fixutres/
    model-comments.json
  controllers/
  models/
    comment.js

Where the app.js is the entry point of your application. db.js is the utility file that you just created above.

Let’s see how the model comment.js might look like. It will store and retrieve user comments.

var DB = require('../db')

var COLLECTION = 'comments'

// Get all comments

exports.all = function(cb) {
  db = DB.getDB()
  db.collection(COLLECTION).find().toArray(cb)
}

// Create new comment and return its id

exports.create = function(user, text, cb) {
  db = DB.getDB()
  db.collection(COLLECTION).insert({user: user, text: text}, function(err, docs) {
    if (err) return cb(err)
    cb(null, docs[0]._id)
  })
}

// Remove a comment

exports.remove = function(id, cb) {
  db = DB.getDB()
  db.collection(COLLECTION).remove({_id:id}, function(err, result) {
    cb(err)
  })
}

As you can see there are methods to create, remove and retrieve user comments. Nothing special, but they all need to be tested.

For this, you will need some data to populate your database before running each test. Let’s use the following JSON file for that, which is stored at tests/fixtures/model-comments.json.

{
  "collections": {
    "comments": [
      {
        "user": "Peter Parker",
        "text": "I like ice cream."
      },
      {
        "user": "John Doe",
        "text": "Nodejs is the best."
      },
      {
        "user": "Peter Jones",
        "text": "Keep your keyboard clean."
      }
    ]
  }
}

Now that we have everything in place: a database, a model to test and data to test the model with, let’s see how it all comes together.

You will put up some tests which will check whether the Comment model above is correct. Here is how the file in tests/models/comment.js might look like.

var should = require('should')
  , DB = require('../../db')
  , fixtures = require('../fixtures/model-comments')

var Comment = require('../../models/comment')

describe('Model Comment Tests', function() {

  before(function(done) {
    DB.connect(DB.MODE_TEST, done)
  })

  beforeEach(function(done) {
    DB.drop(function(err) {
      if (err) return done(err)
      DB.fixtures(fixtures, done)
    })
  })

  it('all', function(done) {
    Comment.all(function(err, comments) {
      comments.length.should.eql(3)
      done()
    })
  })

  it('create', function(done) {
    Comment.create('Famous Person', 'I am so famous!', function(err, id) {
      Comment.all(function(err, comments) {
        comments.length.should.eql(4)
        comments[3]._id.should.eql(id)
        comments[3].user.should.eql('Famous Person')
        comments[3].text.should.eql('I am so famous!')
        done()
      })
    })
  })

  it('remove', function(done) {
    Comment.all(function(err, comments) {
      Comment.remove(comments[0]._id, function(err) {
        Comment.all(function(err, result) {
          result.length.should.eql(2)
          result[0]._id.should.not.eql(comments[0]._id)
          result[1]._id.should.not.eql(comments[0]._id)
          done()
        })
      })
    })
  })
})

First, you load your database utilities and the sample data. Then before running any test you establish a database connection in test mode. This happens inside the before method, which is called only once before any test is run.

Then, before each test you clear the database and load the fixtures. Again you are using the convenience method beforeEach, provided by Mocha, which runs before every one of your tests.

As a result at the beginning of each test you always have the same predictable state.

Best of all, you don’t modify your main database and you can always return to it in the state you left it before running the tests.

With the help of just one simple file, you have solved all the problems mentioned at the beginning.

Everything you saw above is not only useful when you are testing models, but it is also useful when you are testing anything which modifies the database in some way.

Next

You can apply immediately what you saw to your models and remove them of any existing bug while still preventing future bugs.

It will also make you more confident in your code you ship and your overall quality will improve.

While having JSON files to load each state is very convenient, as your application grows in complexity and size you will see that new problems will emerge. Your JSON files will become very large and you will need many of them as your many tests will require different initial state.

To solve this problem you can create a special fixture class, which can build whatever database state you need.

To see how this work, first try it with a small project, 2-3 models for example, and you will quickly see how useful it can be.


Other articles that you may like

Did you like this article?

Please share it

We are Stefan Fidanov & Vasil Lyutskanov. We share actionable advice about development with Node, Express, React and other web & mobile technologies.

It is everything that we have learned from years of experience working with customers from all over the world on projects of all sizes.

Let's work together
© 2024 Terlici Ltd · Terms · Privacy