8 min read

There are different ways to implement Unit Tests for a Node.js application. Most of them use Mocha, for their test framework, Chai as the assertion library, and some of them include Istanbul for Code Coverage. We will be using those tools, not entering in deep detail on how to use them but rather on how to successfully configure and implement them for a Sails project.

1) Creating a new application from scratch (if you don’t have one already)

First of all, let’s create a Sails application from scratch. The Sails version in use for this article is 0.12.3. If you already have a Sails application, then you can continue to step 2.

Issuing the following command creates the new application:

$ sails new sails-test-article

Once we create it, we will have the following file structure:

./sails-test-article
├── api
│   ├── controllers
│   ├── models
│   ├── policies
│   ├── responses
│   └── services
├── assets
│   ├── images
│   ├── js
│   │   └── dependencies
│   ├── styles
│   └── templates
├── config
│   ├── env
│   └── locales
├── tasks
│   ├── config
│   └── register
└── views

2) Create a basic test structure

We want a folder structure that contains all our tests. For now we will only add unit tests. In this project we want to test only services and controllers.

Add necessary modules

npm install --save-dev mocha chai istanbul supertest

Folder structure

Let’s create the test folder structure that supports our tests:

mkdir -p test/fixtures test/helpers test/unit/controllers test/unit/services

After the creation of the folders, we will have this structure:

./sails-test-article
├── api
[...]
├── test
│   ├── fixtures
│   ├── helpers
│   └── unit
│       ├── controllers
│       └── services
└── views

We now create a mocha.opts file inside the test folder. It contains mocha options, such as a timeout per test run, that will be passed by default to mocha every time it runs. One option per line, as described in mocha opts.

--require chai
--reporter spec
--recursive
--ui bdd
--globals sails
--timeout 5s
--slow 2000

Up to this point, we have all our tools set up. We can do a very basic test run:

mocha test

It prints out this:

0 passing (2ms)

Normally, Node.js applications define a test script in the packages.json file. Edit it so that it now looks like this:

"scripts": {
   "debug": "node debug app.js",
   "start": "node app.js",
   "test": "mocha test"
}

We are ready for the next step.

3) Bootstrap file

The boostrap.js file is the one that defines the environment that all tests use. Inside it, we define before and after events. In them, we are starting and stopping (or ‘lifting’ and ‘lowering’ in Sails language) our Sails application. Since Sails makes globally available models, controller, and services at runtime, we need to start them here.

var sails = require('sails');
var _ = require('lodash');

global.chai = require('chai');
global.should = chai.should();

before(function (done) {

// Increase the Mocha timeout so that Sails has enough time to lift.
this.timeout(5000);

sails.lift({
   log: {
     level: 'silent'
   },
   hooks: {
     grunt: false
   },
   models: {
     connection: 'unitTestConnection',
     migrate: 'drop'
   },
   connections: {
     unitTestConnection: {
       adapter: 'sails-disk'
     }
   }
}, function (err, server) {
   if (err) returndone(err);
   // here you can load fixtures, etc.
   done(err, sails);
});
});

after(function (done) {
// here you can clear fixtures, etc.
if (sails && _.isFunction(sails.lower)) {
   sails.lower(done);
}
});

This file will be required on each of our tests. That way, each test can individually be run if needed, or run as a whole.

4) Services tests

We now are adding two models and one service to show how to test services:

Create a Comment model in /api/models/Comment.js:

/**
* Comment.js
*/

module.exports = {
attributes: {
   comment: {type: 'string'},
   timestamp: {type: 'datetime'}
}
};

/**
* Comment.js
*/

module.exports = {
attributes: {
   comment: {type: 'string'},
   timestamp: {type: 'datetime'}
}
};

/**
* Comment.js
*/

module.exports = {
attributes: {
   comment: {type: 'string'},
   timestamp: {type: 'datetime'}
}
};

/**
* Comment.js
*/

module.exports = {
attributes: {
   comment: {type: 'string'},
   timestamp: {type: 'datetime'}
}
};

Create a Post model in /api/models/Post.js:

/**
* Post.js
*/

module.exports = {
attributes: {
   title: {type: 'string'},
   body: {type: 'string'},
   timestamp: {type: 'datetime'},
   comments: {model: 'Comment'}
}
};

Create a Post service in /api/services/PostService.js:

/**
* PostService
*
* @description :: Service that handles posts
*/

module.exports = {
getPostsWithComments: function () {
   return Post
     .find()
     .populate('comments');
}
};

To test the Post service, we need to create a test for it in /test/unit/services/PostService.spec.js.

In the case of services, we want to test business logic. So basically, you call your service methods and evaluate the results using an assertion library. In this case, we are using Chai’s should.

/* global PostService */

// Here is were we init our 'sails' environment and application
require('../../bootstrap');

// Here we have our tests
describe('The PostService', function () {

before(function (done) {
   Post.create({})
     .then(Post.create({})
       .then(Post.create({})
         .then(function () {
           done();
         })
       )
     );
});

it('should return all posts with their comments', function (done) {
   PostService
     .getPostsWithComments()
     .then(function (posts) {
       posts.should.be.an('array');
       posts.should.have.length(3);
       done();
     })
     .catch(done);

});

});

We can now test our service by running:

npm test

The result should be similar to this one:

> [email protected] test /home/lobo/dev/luislobo/sails-test-article
> mocha test



  The PostService
    ✓ should return all posts with their comments


  1 passing (979ms)

5) Controllers tests

In the case of controllers, we want to validate that our requests are working, that they are returning the correct error codes and the correct data.

In this case, we make use of the SuperTest module, which provides HTTP assertions.

We add now a Post controller with this content in /api/controllers/PostController.js:

/**
* PostController
*/

module.exports = {
getPostsWithComments: function (req, res) {
   PostService.getPostsWithComments()
     .then(function (posts) {
       res.ok(posts);
     })
     .catch(res.negotiate);
}
};

And now we create a Post controller test in: /test/unit/controllers/PostController.spec.js:

// Here is were we init our 'sails' environment and application

var supertest = require('supertest');

require('../../bootstrap');

describe('The PostController', function () {

var createdPostId = 0;

it('should create a post', function (done) {
   var agent = supertest.agent(sails.hooks.http.app);
   agent
     .post('/post')
     .set('Accept', 'application/json')
     .send({"title": "a post", "body": "some body"})
     .expect('Content-Type', /json/)
     .expect(201)
     .end(function (err, result) {
       if (err) {
         done(err);
       } else {
         result.body.should.be.an('object');
         result.body.should.have.property('id');
         result.body.should.have.property('title', 'a post');
         result.body.should.have.property('body', 'some body');
         createdPostId = result.body.id;
         done();
       }
     });
});

it('should get posts with comments', function (done) {
   var agent = supertest.agent(sails.hooks.http.app);
   agent
     .get('/post/getPostsWithComments')
     .set('Accept', 'application/json')
     .expect('Content-Type', /json/)
     .expect(200)
     .end(function (err, result) {
       if (err) {
         done(err);
       } else {
         result.body.should.be.an('array');
         result.body.should.have.length(1);
         done();
       }
     });
});

it('should delete post created', function (done) {
   var agent = supertest.agent(sails.hooks.http.app);
   agent
     .delete('/post/' + createdPostId)
     .set('Accept', 'application/json')
     .expect('Content-Type', /json/)
     .expect(200)
     .end(function (err, result) {
       if (err) {
         returndone(err);
       } else {
         returndone(null, result.text);
       }
     });
});

});

After running the tests again:

npm test

We can see that now we have 4 tests:

> [email protected] test /home/lobo/dev/luislobo/sails-test-article
> mocha test



  The PostController
    ✓ should create a post
    ✓ should get posts with comments
    ✓ should delete post created

  The PostService
    ✓ should return all posts with their comments


  4 passing (1s)

6) Code Coverage

Finally, we want to know if our code is being covered by our unit tests, with the help of Istanbul.

To generate a report, we just need to run:

istanbul cover _mocha test

Once we run it, we will have a result similar to this one:

The PostController
    ✓ should create a post
    ✓ should get posts with comments
    ✓ should delete post created

  The PostService
    ✓ should return all posts with their comments


  4 passing (1s)

=============================================================================
Writing coverage object [/home/lobo/dev/luislobo/sails-test-article/coverage/coverage.json]
Writing coverage reports at [/home/lobo/dev/luislobo/sails-test-article/coverage]
=============================================================================

=============================== Coverage summary ===============================
Statements   : 26.95% ( 45/167 )
Branches     : 3.28% ( 4/122 )
Functions    : 35.29% ( 6/17 )
Lines        : 26.95% ( 45/167 )
================================================================================

In this case, we can see that the percentages are not very nice. We don’t have to worry much about these since most of the “not covered” code is in /api/policies and /api/responses.

You can check that result in a file that was created after istanbul ran, in ./coverage/lcov-report/index.html.

If you remove those folders and run it again, you will see the difference.

rm -rf api/policies api/responses
istanbul cover _mocha test                                                                                                                                                               
⬡ 4.4.2 [±master ●●●]

Now the result is much better: 100% coverage!

  The PostController
    ✓ should create a post
    ✓ should get posts with comments
    ✓ should delete post created

  The PostService
    ✓ should return all posts with their comments


  4 passing (1s)

=============================================================================
Writing coverage object [/home/lobo/dev/luislobo/sails-test-article/coverage/coverage.json]
Writing coverage reports at [/home/lobo/dev/luislobo/sails-test-article/coverage]
=============================================================================

=============================== Coverage summary ===============================
Statements   : 100% ( 24/24 )
Branches     : 100% ( 0/0 )
Functions    : 100% ( 4/4 )
Lines        : 100% ( 24/24 )
================================================================================

Now if you check the report again, you will see a different picture:

Coverage report

You can get the source code for each of the steps here.

I hope you enjoyed the post!

Reference

About the author

Luis Lobo Borobia is the CTO at FictionCity.NET, mentor and advisor, independent software engineer, consultant, and conference speaker. He has a background as a software analyst and designer—creating, designing, and implementing software products and solutions, frameworks, and platforms for several kinds of industries. In the last few years, he has focused on research and development for the Internet of Things using the latest bleeding-edge software and hardware technologies available.

1 COMMENT

LEAVE A REPLY

Please enter your comment!
Please enter your name here