Unit Test Your JavaScript!


MV* frameworks/libraries have empowered web developers to build fancy JS-heavy applications like it's going out of style. With great power comes great responsibility and, sadly, many of these apps don't have any meaningful unit tests. Those sparkling new JS apps are really legacy code liabilities of the worst kind.

There are many reasons for this. I think it's mostly due to a lack of time and assumptions that it's not practical. This attitude lingers from the "hacking with jQuery" era when everything was coupled tightly to the DOM and not very testable. What's important to remember is that those fancy MV* frameworks exist to separate your concerns so (if you stick to the patterns) you should at least be able to unit test the Model/ViewModel parts of your app without any DOM entanglements.

If your MV* solution doesn't make testing super easy, then consider using a different one.

Many professional developers I meet want to test their JS but need help getting started. I wanted to share a quick tutorial on Jasmine, which is widely used today because of its readable syntax and extensibility. This will teach you the basics of Jasmine and show you how to run tests via the command line and Visual Studio.

Getting started with Jasmine

There are a few ways to get Jasmine. This tutorial uses 2.0 syntax. To get your feet wet, just download the distribution zip file. Unzip it locally and open the SpecRunner.html file, which is already set up to run some sample tests (in Jasmine often tests are referred to as "specs" due to its BDD approach). Check those out to get a taste.

You can copy-paste each example below into a <script> tag in the SpecRunner.html file to see them in action or just grab them all in this gist.

describe / it

With Jasmine, you use describe() and it() functions to organize your tests in a readable way.


// calculator.js
function isEven(num) {
	return num % 2 === 0;
}

// calculator-tests.js
describe("isEven", function() {
	it("returns true for even numbers", function() {
		expect(isEven(8)).toEqual(true);
	});
	it("returns false for odd numbers", function() {
		expect(isEven(7)).toEqual(false);
	});
});

Each describe function is has its own scope (of course) and can have beforeEach an afterEach hooks for logic to run before and after each it. You can nest describe functions to chain preconditions, which can be handy.


// customer.js
function Customer() {
    this.isHappy = false;
}
Customer.prototype.giveDiscount = function(amount) {
    if(amount > 100){
        this.isHappy = true;
    }
};

// customer-tests.js
describe("Customer", function() {
    var sut;
    beforeEach(function(){
        sut = new Customer();
    });
    it("is not happy by default", function() {
        expect(sut.isHappy).toEqual(false);
    });
    describe("who gets a discount less than 100", function(){
        beforeEach(function(){
            sut.giveDiscount(1);
        });
        it("is still not happy", function() {
            expect(sut.isHappy).toEqual(false);
        });
    });
    describe("who gets a discount more than 100", function(){
        beforeEach(function(){
            sut.giveDiscount(101);
        });
        it("is happy", function() {
            expect(sut.isHappy).toEqual(true);
        });
    });
});

matchers

That expect(something).toEqual(somethingElse) is an example of a matcher. You can create your own matchers to cut down on the verbosity of using the default ones.


// player.js
function Player() {
    this.health = 100;
}
Player.prototype.takeHit = function(damage) {
    this.health -= damage;
};

// player-matchers.js
var playerMatchers = {
    toBeDead: function(){
        return {
            compare: function(actual) {
                var result = {
                    pass : actual.health <= 0
                };
                if(!result.pass){
                    result.message = "I'm not dead yet!";
                }
                return result;
            }
        }
    }
};

// player-tests.js
describe("Player", function() {
    beforeEach(function(){
        jasmine.addMatchers(playerMatchers);
    });
    it("is dead when it takes more than 100 damage", function() {
        var player = new Player();
        player.takeHit(101);
        expect(player).toBeDead();
    });
});

mocking/stubbing/etc with spies

Jasmine has spies which allow you to stub, mock and monitor collaborating objects. All spies are automatically reset after each it function.


// integration.js
var service = {
    getData: function(query) {
        // imagine some insane complexity here
    }
};
var app = {
    showData: function(param) {
        return service.getData({ id: param }).toUpperCase();
    }
}

// integration-tests.js
describe("when an app shows data", function() {
    var result, 
        FAKE_DATA = "super fake";
    beforeEach(function(){
        spyOn(service, "getData").and.returnValue(FAKE_DATA);
        result = app.showData(100);
    });
    it("passes the correct args to service.getData", function() {
        expect(service.getData).toHaveBeenCalledWith({id: 100});
    });
    it("returns the uppercase'd result from service.getData", function() {
        expect(result).toEqual(FAKE_DATA.toUpperCase());
    });
});

async tests

If you ever use setTimeout in a test, odds are it will fail because the assertion executes before your async logic completes. Jasmine provides the done callback to handle async tests, but they are as slow as they would be in real life. You can optionally speed up time by using jasmine.clock() (this only works for some types of asychronous code mind you).


// laggy.js
function laggy(obj) {
    setTimeout(function(){
        obj.done = true;
    }, 1000);
}

// laggy-tests.js
describe("laggy", function() {
    it("sets done to true (uses done, is slow)", function(done) {
        var obj = {};
        laggy(obj);
        setTimeout(function(){
            expect(obj.done).toEqual(true);
            done();
        }, 1001);
    });
    it("sets done to true (uses clock, is not slow)", function() {
        var obj = {};
        jasmine.clock().install();
        laggy(obj);
        jasmine.clock().tick(1001);
        expect(obj.done).toEqual(true);
        jasmine.clock().uninstall();
    });
});

ajax

There are many libraries to help with AJAX testing. You can download a jasmine-ajax plugin which allows you to define XHR responses (including HTTP response codes) based on URLs.

How you test data access depends on your application. You can avoid needing to directly mock XHR if you abstract your data access logic for example.

Running Jasmine on the command line

So far you've just run tests in a test harness HTML page in a browser. Although it's possible to run them in purely in Node, if you are writing browser-targetted JS, you will want your tests to run in a browser to simulate reality, to enable you to test on multiple browsers, and to leverage in-browser debugging tools.

So if your tests need to run in a browser, how do you make them part of your headless continuous integration build? The best way IMO is by using PhantomJS, a headless browser based on WebKit. PhantomJS comes with some example scripts to run Jasmine tests and report errors as exit codes.

The tricky part is the SpecRunner.html HTML test harness file. Jasmine ships with one, but as your JS code base grows, you'll find yourself constantly updating one giant monolithic HTML file with more <script> includes OR create lots of little HTML test harness pages, both are not ideal.

These days, I think Karma is a great option for running browser-facing code in multiple real browsers and PhantomJS. It dynamically creates the HTML harness page for you based on a config file. It can do a lot more for you, like running automatically when your JS files change, generating code coverage reports, and so on.

Here's how to set up karma with run Jasmine tests PhantomJS and Chrome.

# Install Node & NPM
# cd to your project directory
# Install karma
npm install karma --save-dev
# Install jasmine, phantom and chrome plugins
npm install karma-jasmine karma-phantomjs-launcher karma-chrome-launcher --save-dev
# install the CLI globally
npm install -g karma-cli
# run the config wizard
karma init

This will ask series of simple questions and set up a karma.conf.js file for you. To run karma just run karma start and by default it will run you tests in both Chrome and PhantomJS. It will also re-run itself whenever a file changes.

Karma in action

Karma has a nice configuration file and plenty of options. You can run it with its own CLI or integrate it with a build system like grunt or gulp. Here's a condensed version of my config file (no comments, etc.) to give you an idea:


module.exports = function(config) {
  config.set({
    basePath: '',
    frameworks: ['jasmine'],
    files: [
      'src/**/*.js',
      'tests/**/*.js'
    ],
    exclude: [],
    preprocessors: {},
    reporters: ['progress'],
    port: 9876,
    colors: true,
    logLevel: config.LOG_INFO,
    autoWatch: true,
    browsers: ['Chrome', 'PhantomJS'],
    singleRun: false
  });
};

Running Jasmine in Visual Studio

The current version of Visual Studio 2013 does not ship with any JavaScript unit testing tools. The 2015 version will support grunt/gulp so hopefully this situation will improve very soon. Until then, you can use a couple different extensions to get the job done. I'm not a fan of depending on lots of extentions, especially in large teams, but they're OK as a stop-gap if VS integration is very important to you.

Resharper includes support for running JS tests in a specific target browser or PhantomJS. It runs a small web server and dynamically builds the test harness using ///<reference path="../file.js" /> comments. It integrates the JS tests into it's own Unit Test Sessions window. Resharper isn't free but it's a good investment for many other reasons.

Chutzpah is a free set of extensions which enable you to run JS unit tets (among many other things). It comes in a few flavors: a context-menu option to run tests, and an integration for the Unit Test Explorer. You can also run Chutzpah.exe to execute tests on the command line (just be aware of a crazy bug with Nvidia drivers).

Chutpah test runner

Chutzpah is nice because it's very configurable. You can set up a chutzpah.json file in your solution to configure includes and so on. I've used it in large teams with some success.

Final thoughts

Jasmine is a widely-used, straightforward testing library for JavaScript. There are many ways you can run your tests as well. Don't worry about choosing the wrong test runner. The important thing is to write tests, you can always fidget with them later to work with the test runner du jour.

Some tips if you're new to writing unit tests in general:

Unit test your JavaScript for the sake of your customers, your fellow developers, and your future self when you need to refactor ye olde app from 2014!

[ Archive · Home ]