Unit Testing JavaScript with QUnit
There are many testing frameworks available these days, most of which offer some very cool features, such as automated cross browser testing, etc. But today we’ll take a look at something much simpler, QUnit, which is our all time favorite at Trackets.
Why? Having 50 different testing frameworks to choose from actually isn’t so good, since you’ll be constantly arguing with everyone else about which framework is the best one, and what is the right way to test things. There are also many ways to run the unit tests, many different build frameworks, and just a huge number of choices in general when it comes down to creating a new JS project.
The reason why QUnit is our favorite is because you can skip all of the boilerplate around tools like Grunt. All you need is a web browser and a single .html file, which will serve as a test runner. You can even use a CDN for the QUnit source files and use a service like JSBin to get up an running in zero time. Here’s an example
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>QUnit Example</title>
<link rel="stylesheet" href="//code.jquery.com/qunit/qunit-1.15.0.css">
</head>
<body>
<div id="qunit"></div>
<div id="qunit-fixture"></div>
<script src="//code.jquery.com/qunit/qunit-1.15.0.js"></script>
<script>
test("hello test", function(assert) {
assert.ok(1 == "1", "Passed!");
});
</script>
</body>
</html>
To run the tests, all you need to do is just reload the page. Want to
debug the tests? Just insert a debugger
keyword to trigger a
breakpoint and reload the page. There are zero dependencies, zero setup
time, and the tests run instantly.
QUnit basics QUnit is a very simple library with an easy to remember API, as there are only a few functions you’ll be using.
- module - Defines a new module, which essentially groups a bunch of test cases together.
- test - Creates a new unit test.
- asyncTest - Creates a new asynchronous unit test.
- expect - Sets the number of expected assertions in an asynchronous test.
- start - Resumes the execution of an asynchronous test runner, we’ll talk about this later in this article.
Let’s examine our hello world example to see how to write a simple test using the QUnit API.
test("hello test", function(assert) {
assert.ok(1 == "1", "Passed!");
});
The test
function takes two arguments, the name of the test and the
actual test itself in the form of an anonymous function. The name can be
anything you’d like, and the function should accept a single argument,
which is the assert
object.
In our example we called the assert.ok
function, which simply asserts
that something is true
. There is also assert.equal
for comparing
values (more assertions are documented in the API
documentation, which means we
could’ve written the same example as assert.equal(1, "1", "Passed!");
.
All assertions take an optional argument, which is a short description
of the assertion. If you use one of the provided assert functions, such
as assert.equal
, QUnit will show a nice comparison if the test fails.
The QUnit runner allows you to re-run just a single spec by clicking the “Rerun” link, which will allow you to iterate more quickly in case something goes wrong, since you will be able to focus just on that one failing test.
Testing asynchronous code
Since JavaScript is by its nature asynchronous, you should be able to
test such code. This could be either because of AJAX, some animation
running, or just a simple setTimeout
call. We’ll use the last one to
see how to test asynchronous code.
Here’s a function that will wait 200 milliseconds and then increase a counter and call a callback.
window.bar = 0;
function foo(callback) {
setTimeout(function() {
window.bar++;
callback();
}, 200);
}
You might be tempted to test it by simply attaching a callback with an assertion.
test("hello test", function(assert) {
foo(function() {
assert.equal(window.bar, 1);
});
});
But this won’t work as expected and will result in an error. Expected at least one assertion, but none were run - call expect(0) to accept zero assertions.
The reason for this error is that the test runner will exit immediately after the function is called, since the 200ms delay is asynchronous. The fact that we’ve attached a callback doesn’t do anything here, since QUnit has no way of knowing that we actually expect the callback to get called.
To combat this issue, QUnit provides a way to stop the test runner, and
resume it later. This is done using the obviously named start()
and
stop()
functions.
test("hello test", function(assert) {
stop();
foo(function() {
assert.equal(window.bar, 1);
start();
});
});
The call to stop()
increases a counter, and the call to start()
decreases it. If the counter is more than 0 when the test function
returns, the QUnit runner will keep waiting until the counter goes back
to 0. The reason for this is that you might have multiple asynchronous
operations that you’re waiting for, and multiple assertions. You can
even tell QUnit how many assertions are you expecting using the
expect()
function. Let’s see an example.
test("hello test", function(assert) {
expect(2);
stop();
stop();
var x = 0;
setTimeout(function() {
x++;
assert.equal(x, 1);
start();
}, 100);
setTimeout(function() {
x++;
assert.equal(x, 2);
start();
}, 200);
});
We’re setting up two times, one that fires after 100ms, and second one
that fires after 200ms. Initially we say that we’re expecting 2
assertions, which means that if we mess up our start/stop calls or the
timing is wrong, the test will still fail if only one assertion gets
called. We also call stop()
twice, because we’re going to wait for two
separate asynchronous operations.
The rule of thumb is that there should be the same number of stop()
calls as there is start()
. The only exception is when you use
asyncTest
instead of test
, which starts out with the counter already
set to one. The following two tests are thus equivalent.
test("foo", function(assert) {
stop();
assert.ok(true);
start();
});
asyncTest("bar", function(assert) {
assert.ok(true);
start();
});
Conclusion
While QUnit might not be the shiniest of all the testing frameworks, it works perfectly and requires almost no setup. You can simply open up a JSbin and start prototyping your code while writing the tests, without having to install Node.js, configure Grunt, or do any of those other unproductive setup things.
In the next article we’ll take a look at how to run QUnit from the command line using Phantom.js, and how to create a cross-browser testing setup with QUnit and Karma.js.