Tuesday, June 7, 2011

Javascript Unit testing: how to

Javascript, from its humble origin, it was perceived as a toy but now has gained the status of "serious language". Every "serious language" needs "serious tools" to test and document the code.
In the last months I have done some interesting experiences unit testing Javascript that is worth sharing.
I have used Qunit and MockUpHTTPServer (to fake ajax requests).

Setting Up Qunit
It's very easy:
First of all It's better to wrap all the tests stuff in a directory (for example "tests"). Place this folder on the same level of the library you want to test. Then copy inside the "tests" directory qunit.js and mock.js.
The second one is optional: It is useful to test AJAX call.

Inside the test directory you need a simple html (test.html is a good name). The html is mostly boiler plate code:
<!DOCTYPE html>
<html>
<head>
 <title>QUnit Test Suite</title>
 <link rel="stylesheet" href="qunit.css" type="text/css" media="screen">
 <script type="text/javascript" src="qunit.js"></script>
 <script type="text/javascript" src="mock.js"></script> <!-- optional: only for ajax testing -->

 <!-- support -->
 <script type="text/javascript" src="jquery-1.4.4.js"></script>
 <!-- Your project file goes here -->
 <script type="text/javascript" src="../mylib.js"></script>
 <!-- Your tests file goes here -->
 <script type="text/javascript" src="test.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>

    <div>
    <!-- Your can place here some markup to be tested -->
    </div>
</body>
</html>

This html contains the dependencies (in my case jquery) and the code to test (../mylib.js).
Obviously you need to copy your dependencies here (you can also use a CDN).
Now all the pieces are on the right place. You can launch the tests opening the page test.html. If you use chrome/chromium the page works without problems from your file system.
In Firefox you have to access to the page through a web server. The simplest way is to use the simplewebserver class embedded in the standard python library.

cd mydir
python -m SimpleHTTPServer

and then open a browser on

http://localhost:8000/tests/test.html

Until now we haven't write a single test (to tell the truth we haven't write a single line of mylib.js either). Let's develop some tests and simple functions in an TDD fashion.

Writing simple tests
Writing tests is simple. Just open test.js and write:

test('Example', function (){
    var result = mylib.add(3,5);
    ok(result == 8,'3 + 5 is equal to 8');
});

If you run the tests they will fail because there isn't a function named mylib.add. Let's create it in mylib.js:

var mylib = {
    add:function (x,y){}
};

Now the test runs but fail. Finally finish to implement the function:

var mylib = {
    add : function (x, y){
        return x + y;
    }
};

Now the test pass: Hurrah !

If you want, you can wrap a group of tests into a module. You have to place this row between the groups of tests.

module('Module A');
test('Test 1', ...
test('Test 2', ...
test('Test 3', ...
module('Module B');
...

In the previous example the function "ok" was an assertion. When an assertion fails the test show it colored in red. It means that something goes wrong !
The last argument of every assertions is a string that describe what the test is for.
Qunit gives to us various kind of assertions:

  • ok(condition, string) - pass the test if the condition is true
  • equals(result1,result2, string) - pass if result1 and result2 are equals.
We can rewrite our example using "equals":

test('Example', function (){
    var result = mylib.add(3,5);
    equals(result,8,'3 + 5 is equal to 8');
});

If you compare two mutable objects with "equals" you must pay attention: if the objects contain the same content but are actually two different object they are not considered equals.
For example this test will fail:

    var a = {};
    var b = {};
    equals(a,b,'Two different object are not equals so this test fail');

while this test succeed:

    var a = {};
    var b = a;
    equals(a,b,'a and b refers to the same object');

If we are interested in the contents we can use the "same" assertion.

same(result1, result2, string)

This assertion check recursively for equality so this assertion succeed:

same( {a: 1}, {a: 1} , 'passes, objects have the same content');

Another difference between "equals" and "same" is the use of the equality operator: "equals" use the '==' operator while "same" use the '===' operator.
The previous coerce the type of operands. So 0 == false evaluate as true but 0 === false evaluate as false.
Now we can test various functions but ... how can we test interactions between our scripts and the DOM ?

Testing DOM elements
Testing DOM is straighforward. Let's add some mark up to the test.html. For example:
<div id="container" />

Then we write the test in test.js

jQuery(document).ready(function (){
    test('Hello world', function (){
        mylib.hello('world');
        var text = jQuery('#container').text();
        equals(text,'Hello world','The text is Hello world');
    });
});

If you want to test the DOM you have to be sure the dom is entirely loaded so you must wrap your test in the ready event.
Ok now the code (mylib.js):

var mylib = {
    add : function (x, y){
        return x + y;
    },
    hello: function (text){
        jQuery('#container').text('Hello ' + text);
    }
};

The test should pass.

Test asynchronous events
Javascript in your browser run inside an event loop. Scripts are most frequently registered as callback that run when some event trigger.
You can often predict when an event trigger. For example:

mylib.js
var mylib = {
    ...
    click_container: function (){
        jQuery('#container').click(function (){
            jQuery(this).text('already clicked');
        });
    }
}

test.js
jQuery(document).ready(function (){
    test('Click the container', function (){
        jQuery('#container').click(); // trigger the event
        var text = jQuery('#container').text(); // read the text on the dom
        equals(text,'already clicked','Change the container's text');
    });
});

There are cases  you can't predict when an event will trigger. Every time the event is triggered by the browser. For example: functions scheduled with setInterval and setTimeout and AJAX callbacks.
(Pay attention !!!! this case applies also for animations: they are usually done with setInterval or using requestAnimationFrame event).
Qunit has a special testcase called asyncTest. "asynctest" schedule the test to run when the function "start" is invoked:

    asyncTest('Hide with an animation', function (){
        mylib.hello_hide();
        setTimeout(function (){
            ok(jQuery('#container').is(':invisible'),'Hello world is hidden');
            start();
        },400);
    });

    asyncTest('Show with an animation', function (){
        mylib.hello_show();
        setTimeout(function (){
            ok(jQuery('#container').is(':visible'),'Hello world is visible');
            start();
        },400);
    });

The test function call our functions. Then it waits until the animation ends (we are using setTimeout). At the end we call "start" so Qunit can go ahead with the tests.

Let's implement the hello_hide function:

var mylib = {
    ...
    hello_hide: function(){
        jQuery('#container').fadeOut();
    },
    hello_show: function(){
        jQuery('#container').fadeIn();
    }
};

Testing ajax
Testing AJAX is the most trickiest part. Not only the callback is called asynchronously but it interact with the server. The number one rule of unit testing is avoid interaction between the code we are testing and the the rest of the program.
We can solve this issue using a mockup object and substitute it to the XMLHttpRequest object used for AJAX requests. This is a job for MockHttpRequest (https://github.com/philikon/MockHttpRequest).
On top of our test.js we put this snippet:

//setting up the mock ups
var request = new MockHttpRequest();
var server = new MockHttpServer();
server.handle = function (request) {
    request.setResponseHeader("Content-Type", "text/html");
    request.receive(200, "hello world from ajax!");
};
server.start();

This piece of code create a fake XMLHTTPRequest object (request). The "server" object handle ajax call and the method start substitute the original XMLHTTPRequest object with the mock (there is also a method stop to restore the original object).
Let's write the test:

...
    // test using timeout
    asyncTest('Example ajax', function (){
        mylib.example_ajax('/hello');
        setTimeout(function (){
            equals(jQuery('#container').text(),'hello world from ajax!','contains hello world from ajax');
            start();
        },200);
    });
...

This is the implementation:

var mylib = {
    ...

    example_ajax: function(url){
        jQuery('#container').load(url);
    }
};


Useful tricks
If the function you are testing has callbacks it can be used instead setTimeout. For example let's test the jQuery.ajax function:

    asyncTest('Example ajax with callback', function (){
        jQuery.ajax({
            url:'/hello',
            success:function (data, textStatus, XMLHttpRequest){
                equals(data,"hello world from ajax!",'success ajax');
                start();
            }
        });
    });

It's possible to configure your mock up handler to simulate various kind of response (put a glance over the docs for more informations):

//setting up the mock ups
var request = new MockHttpRequest();
var server = new MockHttpServer();
server.handle = function (request) {
    request.setResponseHeader("Content-Type", "text/html");
    switch (request.urlParts.file){
        case 'hello':
            request.receive(200, "hello world from ajax!");
            break;
        case 'broken':
            request.receive(404, "Nothing here");
            break;
        default:
            request.receive(200, "Default");
    }
};
server.start();

Epilogue
Just a couple of rows to explain why unit testing and why is better to write the tests and the code at the same time.
- you can't trust in untested code
- you can refactor your code and launch the test to be sure you haven't broke the code
- code simple to test is usually simple to read and more modular than untested one