Unit Testing in JavaScript - Mocha, Chai and Sinon - a Beginner's Guide

15 minutes

Writing tests is an important part of software development process. Unit tests form a core part of testing process where each functional block is tested as an independent unit.

In this particular guide we are going to write tests using mochajs. We will also use a few helper libraries like chaijs for simpler assertions and sinonjs for stubbing method calls.

Using this setup, tests can be run from the browser as well as from the command line using mocha-phantomjs which I will cover towards the end of this guide.

Posted in these interests:
h/javascript27 guides
h/webdev60 guides

Let us start with a simple javascript class that deals with Car objects.

var Car = function() {
  var wheels = 4,
    fuel = 16; 
  this.color;
  this.getWheels = function() {
    return wheels;
  }
  this.getFuel = function() {
    return fuel;
  }
  this.getColor = function() {
    if (!this.color) {
      throw new Error("Color has not been set yet");
    }
    return this.color;  
} this.setColor = function(color) { if (['Red', 'Green', 'Blue'].indexOf(color) >== 0) { this.color = color; } else {
throw new Error("I am very picky about my car colors." + "Please pass in either Red, Green or Blue"); } } }; //Create a new Car instance var car1 = new Car();

Unit tests deal with testing individual functional blocks of code. Some functional logic that we would like to test can be: 1. Make sure a car has 4 wheels 2. Make sure a car has positive fuel 3. Make sure an error is thrown when we try to get color of a colorless car. 4. Make sure we setColor for car within a limited set of available colors. NOTE: Unit tests should test both positive and negative outcomes. Keeping this list short for brevity.

Download the latest version of mochajs from here, chaijs from here and sinonjs from here. Save them as mocha.js, chai.js and sinon.js respectively.

Also download the css used by mocha from here and save it as mocha.css

You might also refer to these files from a CDN like https://cdnjs.com/ as used below

Now create a car-test.js file that will hold the unit tests for our Car object.

Since we are running these tests from our browser to start with, we will create a unitTests.html file.

This file should include links to the different libraries and test files that we have created as shown below.

<head>
  <meta charset="UTF-8">
  <link href="mocha.css" rel="stylesheet" />
</head>
<body>
  <!-- mocha needs this div to run -->
  <div id="mocha"></div>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.0.0-alpha1/jquery.min.js"></script>
  <script src="car.js"></script>
  <script src="http://cdnjs.cloudflare.com/ajax/libs/mocha/2.3.4/mocha.min.js"></script>
  <script src="http://cdnjs.cloudflare.com/ajax/libs/chai/3.4.1/chai.min.js"></script>
  <script src="http://cdnjs.cloudflare.com/ajax/libs/sinon.js/1.15.4/sinon.min.js"></script> 
  <script src="car-test.js" defer></script>
  <title>Mocha Unit Tests</title>
  <script>
    // Use mocha in Behaviour-Driven Development setup
    mocha.setup('bdd');
    // set chai.expect to window for easy access                                                            
window.expect = chai.expect; window.onload = function() {
// Have mocha run all tests when page loads
mocha.run(); } </script> </body>

Notice above that we have set expect method to window, to make it easily accessible

Note If you run mocha from command line (not covered here) it looks for test in this directory:

./tests/*.js

So you may want to put your test in tests folder and reference it appropriately in corresponding html file.

Some basic unit tests

The tests we mentioned above can be written like this in the car-test.js file.

describe("Car Constructor", function() {
  before(function() {
    //Will run before all tests in this block
    this.car = new Car();
  });
  after(function() {
    //Will run after all tests in this block
    delete this.car;
  });
  it("should have 4 wheels", function() {
    expect(this.car.getWheels()).to.equal(4);
  });
  it("should have positive fuel", function() {
    expect(this.car.getFuel()).to.equal(16);                                                                                 
}); it("should throw Error if getColor is called without being set", function() { // Note that here we are passing function directly to expect expect(this.car.getColor).to.throw(Error); }); it("should return a color if set", function() { var color = "Red"; this.car.setColor("Red"); expect(this.car.getColor()).to.be.a("string"); expect(this.car.getColor()).to.equal(color); }); });

Mocha provides us with before and after hooks which will run before and after all tests in a given describe block. We make use of those in the above tests to initialize a car instance that is available only in this test block. In our actual tests we use helper methods available with the chai library to make writing these tests easier.

Note: Out tests can be made up of multiple describe blocks, each having a number of tests. Each of these describe blocks is considered a test block for this tutorial.

If we run these tests in our browser, we see output as shown in the image below, which indicate all our tests have passed.

Note: We can click on arrow next to test and make only that test run. This can also be done in code writing tests like it.only("Run only this test"). Similarly a test can be skipped by writing it as it.skip("Skip this test"). This can make running tests faster during development.

Caution: While this is a very useful trick, make sure you do not commit the .only or .skip with your code. There are several linters that help you avoid this disaster.

Let us consider the block of code below, which are additional methods on the Car object. The driveForward method on a Car instance makes a call to hasEnoughtFuel.

Car.prototype.moveForward = function(distance) {                                                                             
console.log("Move car by " + distance); };

Car.prototype.stayPut = function() { console.log("I do not have enought fuel to move"); };

Car.prototype.hasEnoughFuel = function(fuel, distance) { var deferred = $.Deferred(); $.ajax({ url: "/check_forward_movement_ability.json", type: "GET", }) .done(function(response) { deferred.resolve(response.canMove); }).fail(function(response) { deferred.fail(); }); return deferred.promise(); };

Car.prototype.driveForward = function(distance) { var promise = this.hasEnoughFuel(this.fuel, distance); var self = this; $.when(promise).done(function(canMove) { if (canMove) self.moveForward(distance); else self.stayPut(distance); }); };

When we call

var car = new Car(); 
car.driveForward(5)

The code makes an ajax request to a hypothetical server. When unit testing front end code, we should avoid making actual requests to the server as this will consume unnecessary resources, delay the tests and is outside the scope of unit tests.

A workaround for this is FakeServer provided by sinonjs. This can mocks the actual call to the server and help us keep the testing scope limited to the unit tests. Here is what this implementation looks like..

describe("Car movement", function() {                                                                                                             
beforeEach(function() { this.server = sinon.fakeServer.create(); this.car = new Car(); sinon.stub(this.car, "moveForward"); sinon.stub(this.car, "stayPut"); }); afterEach(function() { this.server.restore(); this.car.moveForward.restore(); this.car.stayPut.restore(); delete this.car; }); it("should call moveForward, when server responds with canMove as True", function(done) { this.server.respondWith("GET", "/check_forward_movement_ability.json", [200, { "Content-Type": "application/json" }, '{ "canMove": true }']); this.car.driveForward(5); this.server.respond(); sinon.assert.calledOnce(this.car.moveForward); //Tell mocha to wait for response, and then run the test //by calling done() callback done(); }); it("should not call moveForward, when server responds with canMove as False", function(done) { this.server.respondWith("GET", "/check_forward_movement_ability.json", [200, { "Content-Type": "application/json" }, '{ "canMove": false }']); this.car.driveForward(5); this.server.respond(); sinon.assert.notCalled(this.car.moveForward); done(); }); });

Note: Here we initialize a new instance of Car for every test run by initializing it in the beforeEach call. This is because the first test calls moveForward on a car instance. So the second test which checks that moveForward is not called, should run on a different instance, or this test may fail on some browsers like PhantomJS.

Running the tests from command line.

There is a neat plughin mocha-phantomjs to run these tests from command line. This is can be installed as an npm package. We need to make the following adjustments to our unitTest.html file once we have mocha-phantomjs

  <script>
    //Related to a mocha-phantomjs issue
    if (window.initMochaPhantomJS) {                                                                                                              
window.initMochaPhantomJS(); } // Use mocha in Behaviour-Driven Development setup mocha.setup('bdd'); // set chai.expect to window for easy access window.expect = chai.expect window.onload = function() { // Have mocha run all tests when page loads mocha.run(); } </script>

Now the tests can be run from command line as

mocha-phantomjs index.html

which prints results on the command line like below

Hope you found this guide useful and are eager to add unit tests to your code. This guide just touches the surface of unit testing. There are a lot more methods available with Mocha, Chai and Sinon and I would encourage you to check them out.

Backbone Marionette Testing and Refactoring by David Sulc is an excellent resource on this topic.

Let me know in comments below if you have any questions or feedback. Thanks for reading.

Learn how to connect to your work or private VPN on Windows.
Ash Ash (362)
6 minutes

Using a VPN can make working from home a breeze. Connecting to a VPN in Windows is simple enough; all you need is a VPN to connect to.