How to Test your JavaScript Code with QUnit

QUnit, developed by the jQuery team, is a great framework for unit testing your JavaScript. In this tutorial, I'll introduce what QUnit specifically is, and why you should care about rigorously testing your code.

What is QUnit

QUnit is a powerful JavaScript unit testing framework that helps you to debug code. It's written by members of the jQuery team, and is the official test suite for jQuery. But QUnit is general enough to test any regular JavaScript code, and it's even able to test server-side JavaScript via some JavaScript engine like Rhino or V8.

If you're unfamiliar with the idea of "unit testing", don't worry. It's not too difficult to understand:

In computer programming, unit testing is a software verification and validation method in which a programmer tests if individual units of source code are fit for use. A unit is the smallest testable part of an application. In procedural programming a unit may be an individual function or procedure.

This is quoted from Wikipedia. Simply put, you write tests for each functionality of your code, and if all of these tests are passed, you can be sure that the code will be bug-free (mostly, depends on how thorough your tests are).

Why You Should Test Your Code

If you haven't written any unit tests before, you probably just apply your code to a website directly, click for a while to see if any problem occurs, and try to fix it as you spot one. There are many problems with this method.

First, it's very tedious. Clicking actually is not an easy job, because you have to make sure everything is clicked and it's very likely you'll miss one thing or two. Second, everything you did for testing is not reusable, which means it's not easy to find regressions. What is a regression? Imagine that you wrote some code and tested it, fixed all the bugs you found, and published it. Then, a user sends some feedback about new bugs, and requests some new features. You go back to the code, fix these new bugs and add these new features. What might happen next is that some of the old bugs come up again, which are called "regressions." See, now you have to do the clicking again, and chances are you won't find these old bugs again; even if you do, it'll take a while before you figure out that the problem is caused by regressions. With unit testing, you write tests to find bugs, and once the code is modified, you filter it through the tests again. If a regression appears, some tests will definitely be failed, and you can easily spot them, knowing which part of the code contains the bug. Since you know what you have just modified, it can easily be fixed.

Another advantage of unit testing is especially for web development: it eases the testing of cross-browser compatibility. Just run your tests on different browsers, and if a problem occurs on one browser, you fix it and run these tests again, making sure it doesn't introduce regression on other browsers. You can be sure that all of the target browsers are supported, once they all pass the tests.

I'd like to mention one of John Resig's projects: TestSwarm. It takes JavaScript unit testing to a new level, by making it distributed. It's a website that contains many tests, anyone can go there, run some of the tests, then return the result back to server. In this way, code can be tested on different browsers and even different platforms really quickly.

How to Write Unit Tests with QUnit

So how do you write unit tests with QUnit exactly? First, you need to set up a testing environment:

The code that is going to be tested should be put into myProject.js, and your tests should be inserted into myTests.js. To run these tests, simply open this HTML file in a browser. Now it's time to write some tests.

The building blocks of unit tests are assertions.

An assertion is a statement that predicts the returning result of your code. If the prediction is false, the assertion has failed, and you know that something has gone wrong.

Here we defined a function, isEven, which detects whether a number is even, and we want to test this function to make sure it doesn't return wrong answers.

We first call test(), which constructs a test case; the first parameter is a string that will be displayed in the result, and the second parameter is a callback function that contains our assertions. This callback function will get called once QUnit is run.

We wrote five assertions, all of which are boolean. A boolean assertion expects its first parameter to be true. The second parameter is also a message that will be displayed in the result.

Here is what you get, once you run the test:

Since all these assertions have successfully passed, we can be pretty sure that isEven() will work as expected.

The assertion has failed because we deliberately wrote it wrong, but in your own project, if the test doesn't pass, and all assertion are correct, you know a bug has been found.

More Assertions

ok() is not the only assertion that QUnit provides. There are other kinds of assertions that are useful when testing your project:

Comparison Assertion

The comparison assertion, equals(), expects its first parameter (which is the actual value) is equal to its second parameter (which is the expected value). It's similar to ok(), but outputs both actual and expected values, making debugging much easier. Like ok(), it takes an optional third parameter as a message to be displayed.

So instead of:

test('assertions', function() {
ok( 1 == 1, 'one equals one');
})

You should write:

test('assertions', function() {
equals( 1, 1, 'one equals one');
})

Notice the last "1", which is the comparing value.

And if the values are not equal:

test('assertions', function() {
equals( 2, 1, 'one equals one');
})

It gives a lot more information, making life a lot easier.

The comparison assertion uses "==" to compare its parameters, so it doesn't handle array or object comparison:

In order to test this kind of equality, QUnit provides another kind assertion: identical assertion.

Identical Assertion

Identical assertion, same(), expects the same parameters as equals(), but it's a deep recursive comparison assertion that works not only on primitive types, but also arrays and objects. Assertions, in the previous example, will all pass if you change them to identical assertions:

Structure Your Assertions

Putting all assertions in a single test case is a really bad idea, because it's very hard to maintain, and doesn't return a clean result. What you should do is to structure them, put them into different test cases, each aiming for a single functionality.

You can even organize test cases into different modules by calling the module function:

Asynchronous Test

In previous examples, all assertions are called synchronously, which means they run one after another. In the real world, there are also many asynchronous functions, such as ajax calls or functions called by setTimeout() and setInterval(). How can we test these kinds of functions? QUnit provides a special kind of test case called "asynchronous test," which is dedicated to asynchronous testing:

There is one thing to watch out for: setTimeout() will always call its callback function, but what if it's a custom function (e.g, an ajax call). How can you be sure the callback function will be called? And if the callback is not called, start() won't be called, and the whole unit testing will hang:

You pass a timeout to stop(), which tells QUnit, "if start() isn't called after that timeout, you should fail this test." You can be sure that the whole testing won't hang and you'll be notified if something goes wrong.

How about multiple asynchronous functions? Where do you put the start()? You put it in setTimeout():

The timeout should be reasonably long enough to allow both callbacks to be called before the test continues. But what if one of the callback isn't called? How can you know that? This is where expect() comes in:

Conclusion

That's all you need to know to get started witih QUnit. Unit testing is a great method to test your code before publishing it. If you haven't written any unit tests before, it's time to get started! Thanks for reading!