Node.js: Testing a Node.js API with Mocha, Async, and Should

Application Overview

The API is broken down into bundles, each bundle is stored in a separate folder and contains a model and a controller.

The application has the following directory structure.

-/bundles/

-/bundles/foo/model.js
-/bundles/foo/controller.js

-/bundles/bar/model.js
-/bundles/bar/controller.js

The controllers extend from a base module called controller.js which implements the create/review/update/delete code.

Testing the API

The rest of this article will concentrate on the DELETE api call.

This operation doesn't delete the model, but sets a sets the flag "isDeleted" against the model to true.

Before the test, the code must perform three operations a CREATE, then a DELETE, and finally a READ where we'll test the isDeleted flag has been set.

Breaking down the tests using Mocha

First describe the test

                    describe('API Deletion', function () {
}
                    
                

This gives a high level description of the tests contained in this file. Everything the code needs to perform the test should be defined and instantiated at the top of this function.

                    describe('Deleting', function () {
    var should = require('should'),
         async = require('async'),
         fs = require('fs'),
         api = require('./api.js');
         //Loop through bundles and describe our next step
}
                

The next step is to loop through all the bundles and describe the test specific to the bundle we want to test

                var bundles = fs.readdirSync('./bundles');
bundles.forEach(function (item) {
    describe(item, function () {
    //Call the before function and populate our database with the data to test against
    }
}
                    

We define a before function that will run the code to create, delete and get the model which we'll later test for the isDeleted flag.

                var fixture = require('./bundles/fixture.js');
    var deleteResponse = null;

    before(function (done) {

	async.waterfall([

	function create(callback) {
	    api.create(fixture, function (error, response, body) {
		var json = JSON.parse(body);
		callback(null, json);
	    });
	},

	function delete(json, callback) {
	    api.delete(item, json._id, function (error, response, body) {
		var deleteResponse = {
		    id: json._id,
		    error: error,
		    body: body
		};
		callback(null, deleteResponse)
	    });
	},

	function get(deleteResponse, callback) {
	    api.getOne(item, deleteResponse.id, function (error, response, body) {

		var getResponse = {
		    id: deleteResponse.id,
		    error: error,
		    response: response,
		    body: body,
		}

		callback(null, getResponse)

	    });
	}

	], function (err, results) {
	    deleteResponse = results;
	    done();
	});
});
//Define the tests
                    

Passing the done callback into the before method allows asynchronous testing of the api.

Using the asyc libraries waterfall function we can make sure that the before function is only completed once all our API calls are finished.

Using async also helps the clarity of the code, the waterfall function allows another developer to see exactly the steps going to be taken in the before function and in what order.

 The complete solution

                describe('Deleting', function () {
    var should = require('should'),
        async = require('async'),
        fs = require('fs'),
        api = require('./api.js');

    //Loop through bundles
    var bundles = fs.readdirSync('./bundles');
    bundles.forEach(function (bundle) {

        describe(bundle, function () {

            //Get dummy data
            var fixture = require('./bundles/fixture.js');
            var deleteResponse = null;

            //Persist dummy data
            before(function (done) {
                async.waterfall([

                function create(callback) {
                    api.create(fixture, function (error, response, body) {
                        var json = JSON.parse(body);
                        callback(null, json);
                    });
                },

                function delete(json, callback) {
                    api.delete(item, json._id, function (error, response, body) {
                        var deleteResponse = {
                            id: json._id,
                            error: error,
                            body: body
                        };
                        callback(null, deleteResponse)
                    });
                },

                function get(removeResponse, callback) {
                    api.getOne(item, removeResponse.id, function (error, response, body) {
                        var objectResponse = {
                            id: removeResponse.id,
                            error: error,
                            response: response,
                            body: body,
                        }
                        callback(null, objectResponse)
                    });
                }],
               //End of waterfall
               function (err, results) {
                    //store results
                    deleteResponse = results;
                    //Tell before function everything is done
                    done();
                });
            });

             //Tests
            it("shouldn't fail", function () {
                deleteResponse.response.statusCode.should.equal(200);
            });

            it("should respond with valid JSON", function () {
                deleteResponse.response.should.be.json;
            });

            it("should be soft deleted", function () {
                var json = JSON.parse(deleteResponse.body);
                json.isDeleted.should.be.equal(true);
            });
        }
        }
    }