[Developer says] Unit tests in JavaScript with Mocha Chai Sinon and Karma

Many testing frameworks exist in JavaScipt nowadays.

The combination that I got hands on is the mocha [1] testing framework combined with the chai [2] assertion library, the sinon [3] mocking framework and the karma [4] test runner.

I thought to gather some resources here on how to work with these tools.

So here it goes!

Setup of the tools

Setup of karma

npm install -g karma-cli

karma init

After answering the displayed questions, such as the testing framework of choice, karma.conf.js file is generated into the same directory.

Setup of karma dependencies

npm init

# package.json file is created

npm install --save-dev mocha chai sinon phantomjs-prebuilt karma karma-mocha karma-chai karma-sinon karma-phantomjs-launcher karma-coverage

Edit the karma configuration

After installing the required dependencies, we should register mocha, chai and sinon into the frameworks configuration array. We can also change the reporters to be used when running the tests. My preference was the dots reporter for displaying the test results and the coverage reporter that came from karma-coverage plugin, for generating coverage reports.

The karma configuration will look like this now.

// Karma configuration
module.exports = function(config) {
    config.set({
        // base path that will be used to resolve all patterns (eg. files, exclude)
        basePath: '',

        // frameworks to use
        // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
        frameworks: ['mocha', 'chai', 'sinon'],

        // list of files / patterns to load in the browser
        files: [
            'src/**/*.js',
            'test/**/*.js'
        ],

        // list of files to exclude
        exclude: [
        ],

        // preprocess matching files before serving them to the browser
        // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
        preprocessors: {
        },

        // test results reporter to use
        // possible values: 'dots', 'progress'
        // available reporters: https://npmjs.org/browse/keyword/karma-reporter
        reporters: ['dots', 'coverage'],

        // web server port
        port: 9876,

        // enable / disable colors in the output (reporters and logs)
        colors: true,

        // level of logging
        // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
        logLevel: config.LOG_INFO,

        // enable / disable watching file and executing tests whenever any file changes
        autoWatch: true,

        // start these browsers
        // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
        browsers: ['PhantomJS'],

        // Continuous Integration mode
        // if true, Karma captures browsers, runs the tests and exits
        singleRun: false,

        // Concurrency level
        // how many browser should be started simultaneous
        concurrency: Infinity
    });
}

Edit the package.json configuration

We can then register the following script into package.json:

"scripts": {
    "karma": "karma start"
},

This will allow karma to run, with the required dependencies installed locally into the node_modules/ directory.

Start karma

npm run karma

If the autoWatch option is enabled and some file patterns are registered into the files option at karma.conf.js, the files matching the registered patterns are been watched for changes and the tests run each time changes occur.

Unit testing

Writing unit tests with Mocha is quite straight forward. Each test unit is implemented in the callback of the describe() function that mocha provides, which can be nested also. A test case resides inside the callback of an it() function.

describe('moduleName', function () {
    describe('#functionName()', function () {
        it('should do something ...', function () {
            // Test code
        });

        ...
    });

    ...
});

Mocha also provides the functions: [before, after, beforeEach, afterEach], called Hooks and when used, run their callbacks before a test, after, as well before and after each test.

Chai can be used as an assertion library and provides three styles of assertions, assert, should and expect.

Finally, Sinon is a mocking library that provides Spies, Stubs and Mocks.

Spies can be used to watch functions and trace their calls during testing.

Stubs are like Spies with the extra ability to be able to specify their behavior during testing.

Mocks are like Spies and Stubs with the extra ability to specify expectations that should be met when the test runs. When an expectation fails the test fails.

Sinon also provides fake timers which can be used to test code that uses time (e.g. Date()) or timeouts, intervals etc ...

Finally it provides a fake XMLHttpRequest as well a fake server which can become usefull when testing code that uses ajax.

Now suppose we have the following code:

var updateTools = {
    SUCCESS_MESSAGE: 'Update succesfull at: ',
    ERROR_MESSAGE: 'Update fail at: ',
    update: function (updateData) {
        $.ajax({
            method: 'POST',
            url: '/update',
            data: {
              updateData: updateData
            }
        }).always(this.alwaysHandler);
    },
    alwaysHandler: function (response) {
        var timestamp = Date();
        var message = this.ERROR_MESSAGE;
        try {
            if (response.success) {
                message = this.SUCCESS_MESSAGE;
            }
            this.updateMessage(message);
        }
        catch (error) {
            this.updateMessage(message);
        }
    },
    updateMessage: function (message) {
      $('#message').text(message);
    }
};

We could test it like this:

describe('updateTools', function () {
  var testInput = [
    'Test',
    0,
    1,
    -1,
    null,
    undefined
  ];

  describe('#updateMessage()', function () {
    it('should add the given message to a target DOM element', function () {
      var $textStub = sinon.stub($.fn, 'text');

      testInput.forEach(function (input) {
        updateTools.updateMessage(input);

        assert.isTrue($textStub.calledWithExactly(input));

        $textStub.reset();
      });

      $textStub.restore();
    });
  });

  describe('#alwaysHandler()', function () {
    it('should handle the ajax response', function () {
      var updateMessageSpy = sinon.spy(updateTools, 'updateMessage');

      var response = {};
      updateTools.alwaysHandler(response);
      assert.isTrue(updateMessageSpy.calledWithExactly(updateTools.FAIL_MESSAGE));

      response.success = false;
      updateTools.alwaysHandler(response);
      assert.isTrue(updateMessageSpy.calledWithExactly(updateTools.FAIL_MESSAGE));

      response.success = true;
      updateTools.alwaysHandler(response);
      assert.isTrue(updateMessageSpy.calledWithExactly(updateTools.SUCCESS_MESSAGE));

      updateMessageSpy.restore();
    });
  });

  describe('#update()', function () {
    var server;
    var responseJson;
    var serverResponse = [
      200,
      { 'Content-Type': 'application/json' },
      ''
    ];
    var doneStub;
    var failStub;

    before(function () {
      server = sinon.fakeServer.create();
      alwaysStub = sinon.stub(updateTools, 'alwaysHandler');
    });
    afterEach(function () {
      alwaysStub.reset();
    });
    after(function () {
      server.restore();
      alwaysStub.restore();
    });

    it('should update successfully', function () {
      // Prepare the response
      var responseJson = {
        success: true
      };
      serverResponse[2] = JSON.stringify(responseJson);

      server.respondWith('POST', '/update', serverResponse);

      var data = {
        foo: 'bar'
      };
      updateTools.update(data);

      server.respond();

      assert.isTrue(alwaysStub.calledOnce);
    });

    it('should update successfully with but with an invalid JSON response', function () {
      // Prepare the response
      var responseJson = {
        foo: 'bar'
      };
      serverResponse[2] = JSON.stringify(responseJson);

      server.respondWith('POST', '/update', serverResponse);

      var data = {
        foo: 'bar'
      };
      updateTools.update(data);

      server.respond();

      assert.isTrue(alwaysStub.calledOnce);
    });

    it('should fail to update', function () {
      // Prepare the response
      serverResponse[0] = 404;

      server.respondWith('POST', '/update', serverResponse);

      var data = {
        foo: 'bar'
      };
      updateTools.update(data);

      server.respond();

      assert.isTrue(alwaysStub.calledOnce);
    });
  });
});

References

[1] http://mochajs.org/

[2] http://chaijs.com/

[3] http://sinonjs.org/

[4] https://karma-runner.github.io/0.13/index.html