In this post, I’ll discuss the whys and hows of Javascript testing; briefly discuss Jasmine, a testing framework; and review some of potential challenges in testing Javascript, which I’ll discuss in more detail in several follow-up posts.
I like Javascript. The language has some great features that make it possible (even fun) to build powerful, interesting programs; as a programmer, I find few things as rewarding as using the language to create a dynamic UI. Of course, it’s no secret that Javascript also comes with unhappy features (some from the browsers, others from the language itself) that make using the language tricky and error-prone. The more Javascript you use, the more likely you are to spend hours painfully debugging some obscure issue in IE7.
There are a few solutions to this dilemma:
- You could just avoid Javascript — stick instead to simple Ajax calls and let the server handle the rest. (Unfortunately, that’s a little like cutting off your web browser’s left hand.)
- You could just write your code and fix problems as they come up. Why worry now? (Because you’ll worry later, possibly late one Saturday night night.)
- You can test your Javascript. More work upfront, fewer problems down the road.
After a painfully long time firefighting Javascript issues, I’ve come to wholeheartedly embrace testing. That’s hardly a controversial position, I admit, but it’s one that doesn’t seem as widespread in the Javascript community the way it has for other languages.
Since I’ve spent the last few months writing a few thousand lines of test code, it seems like a good time to pull my thoughts together and share what I’ve learned. Consider it my holiday gift to all of you
The Why
So why test your Javascript? Well, an easy answers springs to mind:
- Because all your code should be tested, duh. Your Javascript is just as legit as your Ruby or your Python or your PHP. Of course it should have tests.
You don’t have take it only from me that testing is relevant to Javascript: JS ninja John Resig, creator of jQuery, chose to put test frameworks first, before all other subjects, in his excellent new book Secrets of the Javascript Ninja. He clearly thinks it’s important.
However, I’ll go beyond that one (strong) argument. I think that testing Javascript code may be even more urgent than testing server-side code, for two reasons:
- Because Javascript is front-end. Errors in Javascript are particularly visible and annoying to your users. Think of it like our old friend the Uncanny Valley — users will more easily accept an obviously broken site (an error page) than a site that looks normal but mysteriously doesn’t work (a form whose Javascript validation is broken). I don’t have any data to back that up, but I know which one I find more irritating.
- Because debugging Javascript is a pian in the ass. If you’ve ever had to reproduce and debug an intermittent problem seen by users on IE, you know what I mean. Even at their best, Javascript debugging tools aren’t as good as their server-side equivalents; at their worse, it’s just you and alert against the world. Anything you can do to avoid that fate seems worth it.
Let’s get concrete
Let’s take a quick look at three real-world cases in which testing Javascript has saved me from trouble:
- Browser differences: my tests recently turned up two cases in which I used a native method (Object.create) not provided in older environments; I also discovered oddities in IE8′s console.log (it doesn’t support .apply()). Being able to run a test suite on multiple browsers makes it easy to find, reproduce, and solve such problems without spending unnecessary time in IE-land.
- Corner cases: for an image uploading library, I wrote several (non-deterministic) test cases to smoke out corner cases — for instance, simulating an arbitrary number of uploads that randomly either finished, were canceled, or errored. These tests were invaluable, unearthing several rare problems that might have been impossible to solve from user reports alone.
- Code structure: just this month, test-writing helped me identify an overgrown initializer whose expanding DOM setup section warranted its own function. Though originally spurred by my need to test each part independently, splitting that method has improved the overall quality and maintainability of my code — an example of how tests can lead to better software even before the first test case is run.
Onward!
Now that we’ve talked about why we should be testing, let’s turn those newfound intentions into error-finding assertions.
The How
Last spring I wrote my first tests in JSUnit, a now-defunct port of JUnit to Javascript. This fall, I went looking for a more modern, better maintained testing framework, and discovered that there are a number of excellent Javascript testing frameworks, including QUnit, used by jQuery and Facebook and YUI Test, from Yahoo. Many provide familiar features, such as modular test groups and assertion suites; most also offer a browser-based test runner and can be integrated into automated testing.
I ultimately chose Jasmine, a project from Pivotal Labs. It supports all those features (modular testing, assertions, browser UI, integration into continuous testing), and offers two additional features that won me over:
- Mocking: Jasmine provides a built-in mocking framework, allowing you to intercept function calls, guarantee their return values, and write expectations against them. It’s hard to overstate the value of mocking in writing reproducible test cases; it’s the main reason I chose Jasmine. I’ll provide some examples of mocking below.
- Syntax: Jasmine uses an RSpec-like syntax, which I find easy and descriptive. While QUnit breaks modules apart by calling a method between test cases, and YUI Test uses constructors and hashes to define test groups, Jasmine tests follow an easy-to-read structure:
describe("My Test suite", function() {
it("should test something", function() {
// mocks and expectations
})
describe("a testing subgroup", function() {
// more tests
})
})
After writing over two thousand lines of tests with Jasmine, I’ve found it lives more than meets my expectations. (Ha!) So let’s take a look at how it works.
Using Jasmine
If you’ve written tests before (or even if not), Jasmine should be pretty straightforward. Let’s take a quick look at how Jasmine works — after that, you should check out Pivotal’s documentation, which also covers how to set up a test environment and run the tests in your browser.
describe("Jasmine", function() {
it("makes testing JavaScript awesome!", function() {
expect(yourCode).toBe("lots better");
expect(bugLevel).not.toBe("high");
});
});
Here, we see all the basic testing ingredients:
- Test modules: tests are grouped in describe blocks, which can be nested indefinitely deep inside each other. Each block can have its own setup and teardown methods (see below).
- Test cases: tests are registered by the it() method, which takes a description and a function containing the test.
- Assertions: assertions in Jasine come in two parts — expect() and an assertion function, such as toBe(). expect sets up which object or value you’re about to make an assertion about — it can be a variable, raw value, or even a function (see mocking below). Assertions can be negated by adding .not between the expect and the assertion (as above).
Setup and Teardown
We can also set up setup and teardown functions (beforeEach and afterEach) at any level. This is actually quite powerful, allowing you to organize test groups with successive levels of specificity (tests for a given method, tests for that method if the interface isn’t yet loaded, etc.):
describe("ImageUploader", function() {
var imgUploader;
beforeEach(function() {
// clean copy each time
imgUploader = new ImageUploader();
})
it("should define a property", function() {
expect(imgUploader.myProperty).toBeDefined();
})
describe("once intialized", function() {
beforeEach(function() {
imgUploader.init();
})
it("should contain some value", function() {
expect(imgUploader.anArray).toContain(someValue);
})
})
})
Mocks
Mocking is awesome. In Jasmine, mocks are called spies — you spyOn a function, and can then fake its result and see if it was called. A few examples:
describe("ImageUploader", function() {
it("should act a certain way if an image is uploading", function() {
// easily guarantee you're test runs with the expected conditions
spyOn(imgUploader, "isUploading").andReturn(true);
imgUploader.myMethod();
expect(imgUploader.myProperty).toEqual(someValue);
})
describe("when intializing", function() {
it("should notify the upload manager", function() {
<span style="font-size: 11.8056px;"> // this method will be invoked in init</span>
spyOn(uploadManager, "register");
imgUploader.init();
// validate that a method was invoked as expected
expect(uploadManager.register).toHaveBeenCalledWith(imgUploader);
})
it("shouldn't fire an event if the page is still loading", function() {
// another method used in init
spyOn(Page, "isLoaded").andReturn(false);
spyOn(imgUploader.node, "trigger"); // jQuery trigger function
imgUploader.init();
// make sure a function wasn't called
expect(imgUploader.node.trigger).not.toHaveBeenCalled();
})
})
})
Using mocks to control your testing environment is much, much easier than rigging the right conditions each time. If you’ve never used mocks, you should definitely give them a try.
Gotcha for Rubyists: in Jasmine, the order of method calls is different than in RSpec. As in Ruby, you set up your spy behavior first, but you call call expect after executing the relevant code to be tested.
Custom assertions
You can create custom assertions (called matchers) to handle common tests in your code. It’s fairly simple task — I’ll cover how to write custom matchers, with useful examples, in a follow-up post.
Real code
Want to see some real Jasmine tests? Just take a look at the test cases I’ve written for some utility functions in my image uploading library. (If that makes sense, there are a number of other, more complicated tests in the same repository.)
Challenges
This being Javascript, there are some challenges you’re likely to encounter as you write your tests. Some are self-inflicted, others come from the way the language works. In particular, I’m going to cover four areas worthy of their own follow-up posts:
- Developing reusable test cases: basic test blocks are easy, but you can quickly run into Javascript scoping issues if you want to build dynamic tests.
- Testing against browser events: browsers, of course, all behave differently, and you have to do some quick footwork if you want to test code that relies on events.
- Testing encapsulated methods: encapsulation (defining variables in a private scope) is a powerful technique, but it doesn’t sit perfectly well with testing. I don’t have a surefire solution to this problem, but will discuss my attempts to find a balance.
- Overmocking: it’s deceptively easy (I say from experience) to start testing the implementation itself rather than the results, making your tests more brittle. Avoiding overmocking is mainly a matter of awareness, so I’ll present several examples from my own code.
Keep an eye out for those posts early next year!
Conclusion
That’s Javascript testing in a very brief nutshell. If it saves even one reader from the pain of an emergency debugging date with IE6, it’s been worth it.
As always, I’d be happy to answer any questions and would love to see your comments.
Hope you’ve all had happy holidays so far — have a fun, safe, and bug-free New Year!
Alex