Javascript Unit Testing with QUnit and Sinon.js

In this post I will walk through how to get started with some simple unit tests using QUnit as the test framework and Sinon.js as a mocking framework.

A few months ago I wrote about the Google Analytics Event Tracking jQuery Plugin that we released as open source. Recently, Google made an addition to the Google Analytics API which I wanted to add support for in the plugin.

The change was a new boolean flag which signals whether the event was triggered by user interaction or not. This is important since an interactive event affects the bounce rate. It was a few months since I'd worked on the plugin and I was a little wary of changing the code. I knew I really needed some unit tests.

Unit Testing

For those new to the concept of Unit Testing, the basic idea is to write a suite of automated tests which run against your code and test the smallest piece of functionality possible which usually means each method. You define a set of expected outputs for a set of inputs and ensure that the code does what you expect it to do.

A well designed unit test suite, once in place, gives various benefits: you can confidently change code safe in the knowledge that it still does what it was doing before; regressions can be seen immediately; new developers can get a sense of what the code is meant to do and how to use the API; you can design your API without having to write the client code - this makes it easier to divide up work and probably most importantly - coding to enable unit tests requires you to write loosely coupled code which is good for maintainability and flexibility. A poorly designed or incomplete test suite on the other hand can have some negative effects but that's another post!

So, what I wanted to ensure was: by adding support for this change to the GA API that I didn't break any of the code I had written months earlier.

Javascript Unit Testing

In C# I'm (fairly) diligent about unit testing code where possible and am fairly proficient in NUnit. However, unit testing JavaScript is something I've always wanted to do but never really got round to learning. Here was a golden opportunity I thought! There are a lot of JavaScript unit testing frameworks around now that Test Driven Development has become popular and I looked at Jasmine and jsUnit initially but finally decided that QUnit, which was developed to test jQuery, was the best fit.

QUnit provides a simple API. I only really used 4 functions:

  • module which provides access to setup and teardown methods.
  • test which defines a test as you might think.
  • ok an assertion.
  • equal also an assertion.

For those unfamiliar with Unit Testing parlance, an assertion is a test for the expected result. Just like in the English language you make an assertion, which could be true or false, in a unit test you do so with code. The functions setup and teardown provide a way to run code before and after each test is executed, enabling the state to be reset.

The ok assertion just takes a single argument which it expects to be a boolean. If it is true: the test passes, if it is false: the test fails.

The test function takes 2 arguments, the first is the name of the test and the second is a function which is the test. It's imperative to follow a consistent structure with your test names and to be descriptive, remember unit test code is code too. If you don't, you will be kicking yourself later when trying to debug why "mytest234" failed with "Object does not support this property or method".

For example:

test("2 plus 2 should equal 4", function() {
   var expected = 4;
   var result = (2 + 3);
   ok(result == expected,"2 plus 2 was not 4, universe broken. Good luck fixing that.");
});

In the above test you can see that we're using the test function and the ok function. Let's get this working.

Setting up QUnit

Download QUnit and Sinon.js and create an index.html file which will be your test harness like the one below.

 <!DOCTYPE html>
<html>
<head>
	<title>QUnit Test Suite</title>

	<link rel="stylesheet" href="lib/qunit.css" type="text/css" media="screen">

	<script type="text/javascript" src="lib/qunit.js"></script>
	<script type="text/javascript" src="lib/sinon-1.3.2.js"></script>
	<script type="text/javascript" src="lib/sinon-qunit-0.8.0.js"></script>

       lt;!-- Your project file goes here -->

	<!-- Your tests file goes here -->
	<script type="text/javascript" src="tests.js"></script>
</head>
<body>
	<h1 id="qunit-header">QUnit Test Suite</h1>
	<h2 id="qunit-banner"></h2>
	<div id="qunit-testrunner-toolbar"></div>
	<h2 id="qunit-userAgent"></h2>
	<ol id="qunit-tests"></ol>
</body>
</html>

Note we haven't included our project file. Then create a file called tests.js containing the test for "2+2 should equal 4" and save in the same directory as the html above.

Now, run the test by opening the index.html file in your browser.

Oh no, the universe is broken! Is down up? Is north south? No, we made a mistake in our test code: 2+3 does not equal 4. So correct the error and re run the test:

All good.

Creating the real tests

So a real example then. Our Google Analytics plugin can append unobtrusive attributes to an element using the gaTrackEvent method. The call looks like:

$('selector').gaTrackEvent({ Category: 'TestCategory', Action: 'TestAction' });

The code will then fire the event immediately since we have not specified to use an interaction event such as "click". So what how will we test this?

First we need to add the references to the scripts for our project to our index.html test harness:

<!-- Your project file goes here -->
<script type="text/javascript" src="jquery.min.js"></script>
<script type="text/javascript" src="../jquery.building-blocks.googleanalytics.js"></script>

Now, I've written a test below. It creates a div DOM element and then applies tracking to it. I remove the div first to avoid conflicts with other tests.

test('gaTrackEvent adds the category to an element', function() {
      $('#trackMeDiv').remove();
      $('body').append('<div id="trackMeDiv"></div>');

      $('#trackMeDiv').gaTrackEvent({
          category:'TestCategory',
          action:'TestAction'
      });

     equal($('#trackMeDiv').attr('data-ga-category'),'TestCategory','Category attribute did not match expected category');
});

Lets run this.

We're failing. Why? Unit tests must not have external dependencies because then they are not testing a single unit of functionality but are also testing the dependency. This is the most challenging part about writing testable code, making it so that dependencies can be faked or mocked.

However, JavaScript makes mocking and faking easy because of it's dynamic nature, and we can use a framework like Sinon.js to provide us with some additional functionality.

In our code, we have a dependency on the _gaq object which is created by Google Analytics when it is included on the page.

ga: {
  trackEvent: function(args) {
    var defaultArgs = {
     category : 'Unspecified',
     action: 'Unspecified',
     nonInteractive:false
    };
    args = $.extend(defaultArgs,args);
    <strong>_gaq.push(['_trackEvent', args.category, args.action, args.label, args.value, args.nonInteractive]);</strong>
  }
}

We have wrapped this function in a function called gaTrackEvent, which does nothing except apply arguments and call the _gaq object. JavaScript makes faking the _gaq object extremely easy:

var _gaq = {
	push:function() {}
};

Re-run the test and we are now green for GO!

Is this a good test though? What is this telling us?

Well: it tells us the category and attribute are being appended to the element and yes, the push function is being called, but with what?

I could have _gaq.push('the quick brown fox jumped over the lazy developer'); in my gaTrackEvent method and our test wouldn't tell us. So, I think you can agree this is not a good test.

How do we fix it? Well we need some way of examining the arguments that the mocked object is receiving. This is where mocking comes in.

Spying on functions

Sinon has a feature called spying which allows us to examine the arguments a stubbed function was called with. However we need to do add a bit of code to do this. We now add a module just after the declaration of the _gaq object:

module("ga-testing", {
    setup: function() {
      $('body').append('&lt;div id="trackMeDiv"&gt;&lt;/div&gt;');
    },
    teardown: function() {
      $('#trackMeDiv').remove();
    }
});

We can refactor our test and module so the add and remove the trackMeDiv occurs in setup and teardown so any other tests are unaffected by previous ones.

Now, to business. Sinon spies provide loads of functionality including a way of asserting that a function was called with some expected arguments passed to a function like so:

So all we need to do is add the set up to the module setup function:

module("ga-testing", {
    setup: function() {

      // Here we call Sinon to replace the _gaq.push method
      sinon.spy(_gaq,"push");

      $('body').append('&lt;div id="trackMeDiv"&gt;&lt;/div&gt;');
    },
    teardown: function() {

      //Restore control to the original function (this will prevent our tests interfereing with eachother
      _gaq.push.restore();

      $('#trackMeDiv').remove();
    }
});

Now we can use the calledWith functionality to test that push was called with the right arguments. We should add a second test for this because technically we are testing something different than our first test, and as a general rule, you should only have one assertion per test because otherwise it can become difficult to see what is failing.

Here's our second test:

test('gaTrackEvent calls push with the correct arguments when Category and Action are set', function() {
        //This is the arguments you'd pass to _gaq.push if you were calling Google Analytics code directly.
	var expectedArgs = ['_trackEvent', 'TestCategory', 'TestAction', undefined, undefined, false ];

        //Track the div
	$('#trackMeDiv').gaTrackEvent({
		category:'TestCategory',
		action:'TestAction'
	});

        //Were we called with what we thought?
	ok(_gaq.push.calledWith(expectedArgs),'The trackEvent method was not called with the correct arguments!');
});

Note for this second test we don't create a either because that now happens in our setup and teardown functions.

We are creating the expectedArgs array which would normally be passed to Google Analytics and we are using Sinon's calledWith function to check _gaq.push was called with the correct arguments.

Run the test:

Well it ended up being a bit of an epic but I hope you have learned something. I have added quite a few more tests. Please see the code over at github and the project website.

Thanks for reading.

Any questions?

If you need more information or have any questions just get in touch and we'd be happy to answer them for you.