Visit The School

How To Start Unit Testing Your Express Apps

Alexander Paterson
|
Posted about 1 year ago
|
14 minutes
How To Start Unit Testing Your Express Apps
Stop being lazy and start unit-testing your applications

We'll be unit-testing with a commandline tool called Mocha. First I'll teach you how to use it, then I'll introduce you to a framework called Chai which makes things even easier. Finally, we'll explore how we can use these tools to test our express servers and mongoose models.

Mocha

As a node package and a commandline tool, we install Mocha by running the following command:

$ npm install -g mocha

We can now run tests using the mocha command.

To stay organised, create a directory called test/ in the root of the application you're hoping to test.

Tests are written in javascript and are interpreted by the mocha commandline tool, for example: one runs mocha userModel.js, where userModel.js is a file describing the tests that need to be performed using special functions that are a part of Mocha. 

Writing tests is actually quite simple thanks to natural naming conventions, and we don't even need any new packages to get started. We can run tests with mocha functions alone (don't be thrown off by the lack of imports; mocha will take care of that):

// index.js

// This is just for organisation and reporting
describe('Our application', function() {

  // This is the name of the test
  it('should understand basic mathematical principles', function(done) {

    // We want this test to pass.
    if (5 == 5) {
      // If the behavior is as expected, call done with no argument.
      done();
    } else {
      // Otherwise, call done with an error.
      done(new Error("Not sure what's happened."));
    }

  });

});

Executing this gives us the following:

First mocha test

How easy was that? This use of the done callback is essential to asynchronous tests, such as those for database queries and API calls. Tests also pass when they return, and fail when they throw an error, so the following test is equally valid:

// index.js

describe('Our application', function() {

  it('should understand basic mathematical principles', function() {

    // We want tests to pass.
    if (5 == 3) {
      // Hope we don't get here.
      throw new Error("Oh no.");
    }

    // Since no error thrown, test will pass.

  });

});

Before And After Hooks

Unit tests are designed to be numerous and short, so mocha can handle repeated before/after work for us. Note that describe blocks can be embedded in one another for managing scope and grouping tests. See the following example to see how before/after hooks are registered.

I also added a sub-test-group at the end there called (deeper).

// index.js

var express = require('express'); // (npm install --save express)

describe('Our application', function() {
  var app,
      date;

  // Timeout for tests that take time
  this.timeout(5000);

  // Called once before any of the tests in this block begin.
  before(function(done) {
    app = express();
    // Any asynchronous action with a callback.
    app.listen(3000, function(err) {
      if (err) { return done(err); }
      done();
    });
  });

  // Called once before each of the tests in this block.
  beforeEach(function() {
    date = new Date();
  });

  // Called after all of the tests in this block complete.
  after(function() {
    console.log("Our applicationa tests done!");
  });

  // Called once after each of the tests in this block.
  afterEach(function() {
    console.log("The date for that one was", date);
  });

  it('should understand basic mathematical principles', function() {
    // We want tests to pass.
    if (5 == 3) {
      // Hope we don't get here.
      throw new Error("Oh no.");
    }
  });

  it('should understand basic truths', function() {
    // We want tests to pass.
    if (false) {
      // Hope we don't get here.
      throw new Error("Oh no.");
    }
  });

  describe('(deeper)', function() {

    // Called once before any of the tests in this block begin.
    before(function() {
      console.log("Begin going deeper!")
    });

    it('should perform basic math', function() {
      // We want tests to pass.
      if (1+1 != 2) {
        // Hope we don't get here.
        throw new Error("Oh no.");
      }
    });

    it('should perform basic counting', function() {
      // We want tests to pass.
      if ('abc'.length != 3) {
        // Hope we don't get here.
        throw new Error("Oh no.");
      }
    });

  });

});

This prints the following:

Mocha Embedded Tests And Before/After Hooks

And with console.log calls commented out:

Mocha Test Log

Chai

It's your job to pick the library you're going to use on top of mocha; there are many. I'm going to introduce you to Chai, which adds some very enjoyable syntax.

Install Chai with:

$ npm install --save-dev chai

Now we can import this node module into our code and use the expect syntax as follows:

// index.js

var expect = require('chai').expect;

describe('Our application', function() {

  it('should understand basic mathematical principles', function() {

    expect(5).to.equal(5);
    expect(5).to.not.equal(3);

  });

});

This test passes as required. If you need to make an asynchronous call, pass done to the callback argument of it and call it when you're finished testing, or when you hit any errors (either throw errors or pass them to done).

There are endless examples of Chai grammar in the docs, and it supports different language patterns such as should which are more stylistic than anything.

Testing Express Apps

You might be wondering is how to test express applications; it's actually a lot easier than you'd expect thanks to a wonderful node package called supertest. This makes it easy to send API calls directly to your server as a javascript object.

Install it with:

$ npm install --save-dev supertest

Now watch how I create a little server using a new function and a before-hook, then query it using supertest, which I import as request for readability:

// index.js

var express = require('express'); // (npm install --save express)
var request = require('supertest');

function createApp() {
  app = express();

  var router = express.Router();
  router.route('/').get(function(req, res) {
    return res.json({goodCall: true});
  });

  app.use(router);

  return app;
}

describe('Our server', function() {
  var app;

  // Called once before any of the tests in this block begin.
  before(function(done) {
    app = createApp();
    app.listen(function(err) {
      if (err) { return done(err); }
      done();
    });
  });

  it('should send back a JSON object with goodCall set to true', function() {
    request(app)
      .get('/index')
      .set('Content-Type', 'application/json')
      .expect('Content-Type', /json/)
      .expect(200, function(err, res) {
        if (err) { return done(err); }
        callStatus = res.body.goodCall;
        expect(callStatus).to.equal(true);
        // Done
        done();
      });
  });

});

So first you wrap app in a call to request, then you can configure the HTTP request with calls like .get, .post and .set (setting headers), before you call .expect to test certain outcomes.

You'll also want to know how to perform a request with a body; use .send:

request(app)
  .post('/v1/auth/signin')
  .set('Content-Type', 'application/json')
  .send({ email: 'email', password: 'password' })
  .expect('Content-Type', /json/)
  .expect(200, done);

Above, not only have I made a POST request, but I've passed done directly to a call to expect. Read more about supertest here, and check out api-easy for hints about the syntax.

Unit Testing Mongoose Models

Testing Mongoose models can be a bit tricky because the global nature of mongoose connections.

To start with, our lives will be a lot easier if we register our models in a function as follows:

// models.js

exports.registerModels = function() {
  try {
    mongoose.model('user', userSchema);
  } catch (error) {
    // console.log(error)
  }
}

The reason we need the try and catch block is becuase if you attempt to call mongoose.model repeatedly for the same key, it throws an exception. You might be frustrated accidentally calling it multiple times.

Here's an example of me setting up and testing a model:

// index.js

var expect = require('chai').expect
    mongoose = require('mongoose');

describe('Models', function() {
  var User;

  beforeEach(function(done) {
    mongoose.connect('mongodb://localhost/test_mocha_example');
    mongoose.connection.once('connected', () => {
      mongoose.connection.db.dropDatabase();

      require('./models').registerModels();
      // This is the right model because ^registerModels set it up for us.
      User = mongoose.model('user');
      done();
    });
  });

  afterEach(function(done) {
    mongoose.disconnect();
    done();
  });

  describe('Lifecycle', function() {

    it('should not save without password', function(done) {
      var user = new User({
        email: "[email protected]"
      });
      user.save(function(err) {
        expect(err).to.exist
          .and.be.instanceof(Error)
          .and.have.property('message', 'user validation failed');
        done();
      });
    });

  });

});

That should bring everything together for you. When testing routes of your express applications, I'll usually check both the response of the server, and also for any necessary changes in the database.

One last trick: you can run mocha . --recursive to run all test files recursively in the current directory, and you can run mocha . --watch to automatically re-run tests when code changes occur. If you have any questions, ask below.

For good measure, I'll include the models file referenced in the last snippet:

// models.js

var mongoose = require('mongoose'),
    Schema = mongoose.Schema;

// Solves mpromise warning
mongoose.Promise = global.Promise;

var userSchema = new Schema({
  email: {
    type: String,
    unique: true,
    lowercase: true,
    required: 'Email address is required'
  },
  password: {
    type: String,
    required: true
  }
});

exports.registerModels = function() {
  try {
    mongoose.model('user', userSchema);
  } catch (error) {
    // console.log(error)
  }
}


ALEXANDER
PATERSON