Unit Testing in JavaScript

Mark Adelsberger HTML5, JavaScript, Testing 2 Comments

Attention: The following article was published over 8 years ago, and the information provided may be aged or outdated. Please keep that in mind as you read the post.

JavaScript has come a long way. There was a time it was easily dismissed – maybe suitable for noncritical validations, but not much else.

Over the years, the worst of the obstacles to writing large-scale JavaScript programs have fallen away, or at least been mitigated. JavaScript dialects are more consistent among browsers than they once were, and tools like jQuery can help smooth over the differences that still exist. Debuggers are at least not as terrible as they once were (though a lot still depends on what browser you can use to debug).

And in any case, with the advent of HTML5 and mobile platforms, JavaScript is becoming much harder to ignore. Whole application front-ends are being written in JavaScript, and while the tool support lags well behind what we expect in Java or C#, it’s evolving.

One area that remains particularly challenging, and that will become incredibly important as JavaScript projects grow in scope, is unit testing.

Choosing a Framework

If you write Java, you probably don’t spend much time deciding on a unit test framework. I can’t remember the last Java project I worked on that didn’t use jUnit. (Mock object frameworks may be another matter, but I’ll come back to that.) The JavaScript landscape isn’t as settled, and which framework you choose can profoundly affect your testing capabilities.

qUnit is a good place to start. It’s reasonably easy to learn; I found a couple of its behaviors less than intuitive, but once I understood the problems those behaviors were meant to address, everything “clicked” for me. It allows for interaction with the DOM; you can identify a DIV on your test page to be reset to its original state between tests, promoting test atomicity as you can “tee up” a single starting state. And it supports “asynchronous tests,” which I’ll explain shortly; for many apps, this will be a vital feature.

js-test-driver focuses on parallel testing in multiple browsers. It has its own language of test cases and assertions, distinct from qUnit’s. (There are projects that allow the two to be used together, though as of now I’ve not found one that fully supports all capabilities of both tools.) Unlike qUnit, it doesn’t use part of the DOM to report results and it resets the entire DOM between test runs. This imposes one less constraint on the code to be tested.

And these are just two of many options, as a quick search of Wikipedia confirms.

Test Execution Environment

This is another question that, at least in my experience, seems much more complicated with JavaScript than with Java: “In what environment should I run my tests?”

You should always want your test environment to match your production deployment environment as closely as possible. But for many JavaScript apps, a critical piece of that environment – the browser with its attendant JavaScript engine – is chosen by the user. Even if cross-browser issues aren’t as bad as they once were, it would still be folly to test only in one browser and assume the rest will fall in line.

At the same time, testing in even one browser poses challenges when you start thinking about automated testing and CI. (This is the big challenge that js-test-driver is meant to address. If you want a pure qUnit solution, you’ll probably find yourself either writing your own driver scripts or doing without full automation.)

There are easier-to-script JavaScript runtime environments, like NodeJS, which may be useful for some level of automated testing; but these may have their own limitations (NodeJS doesn’t have native DOM support, though add-ons exist) and in any case may not match your target browser’s behavior.

A multi-layered approach might suit your needs. You could script some tests to run in NodeJS for CI, and then periodically launch a more comprehensive (but more manual) test suite in each target browser. Some bugs might not be caught as early as with a full CI suite, of course.

But then, that’s the main take-away: as the tools stand today, unit testing in JavaScript is a world of choices, trade-offs, and compromises.

Code Structure

When jUnit was young, it wasn’t uncommon to find Java code that didn’t submit easily to unit testing. Over the years, this became seen as a test of the quality of a design. JavaScript has, at best, just begun to develop this discipline, and some of the language features that push Java developers in the right direction (like encapsulation) don’t enjoy the same level of native support in JavaScript.

There are frameworks that can help you steer toward writing in testable units. I recently worked with ExtJS, for example, which puts a class system on top of the JavaScript object model. Tools like this can help if you let them, but as they aren’t a native part of the language, you can always choose to go against their grain. Writing testable JavaScript is, and for the foreseeable future will remain, an art you must choose to practice.

I don’t think there’s any one “right” way to do it, but in general moving away from a lot of anonymous inline script and toward functions (either with names or assigned to accessible variables), preferably in separate code files, goes a long way.

Even if you write every line of code with an eye on testability, though, one challenging quirk will emerge in many applications: ubiquity of asynchronous behavior. Every major JavaScript engine is single-threaded, yet common tasks like requesting data from the server (AJAX), responding to the UI, or explicitly delaying an activity (using setTimeout or setInterval) all act asynchronously. Midway through someFunction() you set the ball in motion that will cause anotherFunction() to execute, but anotherFunction() ends up in a queue where it will sit until after someFunction() has returned. There may even be other functions in the queue ahead of anotherFunction().

So what’s a test runner to do? You expect to bracket the code you’re testing.

  1. Framework sets up to run the test
  2. Test code sets preconditions and makes a call to the code to be tested
  3. Code to be tested does something interesting
  4. Test code checks assertions
  5. Framework reports results and moves on to the next test

But what if the code to be tested does something asynchronously? Here’s one way that might play out:

  1. Framework sets up to run the test
  2. Test code sets preconditions and makes a call to the code to be tested
  3. Code to be tested starts to do something interesting, and then it queues up a function to finish the job
  4. Framework reports (inaccurate) results and moves on to the next test
  5. Other queued functions may run
  6. The queued part of the code to be tested gets its turn to run, and finishes the interesting thing that was started in Step 3
  7. Test code checks assertions

At best assertions get reported against the wrong test. More likely you get a bunch of tests failing for no good reason.

qUnit addresses this with the asynchronousTest() function. This works just like the regular test() function, except the framework doesn’t assume the test is over when the test code returns. You have to tell it when the test is over by calling start(). (In practice, test code that’s dealing with asynchronous behavior will consist, in part, of event handlers or other callback functions. This is where you’ll test final assertions and, if there are no more asynchronous events to be triggered, call start().)

If you think this all seems a little backwards, you’re not alone. You call start() when you want to end your test, because it tells the framework to (re)start processing. This whole mechanism causes tests to run in lockstep, which seems synchronous; but you call asynchronousTest() because the name refers to the asynchronous nature of the code you’re testing.

Mock Objects

I suppose this has all seemed a little pessimistic, so let’s end on a ray of sunshine. The dynamic nature of JavaScript gives you all kinds of flexibility to write mock objects, even without using a mock object framework.

In Java, your mock object has to share an interface with, and/or be a subclass of, the class it replaces. Depending on the sophistication of your mocking framework, this can impose various restrictions and/or make you jump through hoops to get the job done. In JavaScript, you can just build an object that implements exactly the mock behavior you want, and it needn’t bear any special relationship to its production counterpart. Of course it still helps if your code is written with inversion of control in mind, but if it’s not then at least you may benefit from the relative ease of intercepting a function call in JavaScript.

Wrapping it Up…

It would be easy to overlook unit testing as you start to embark on large-scale projects in JavaScript, but the need for unit tests is just as real as in any other language. As of today, unit testing tools and procedures aren’t as clear-cut for JavaScript as for Java and other languages that have longer tenures in the full-scale application development world. There are more decisions to make, more trade-offs to consider, and more hurdles to clear.

Hopefully this discussion has you thinking about some of the decisions you’ll have to make.

— Mark Adelsberger, [email protected]

0 0 votes
Article Rating
Notify of
Newest Most Voted
Inline Feedbacks
View all comments