How It Started
If, like me, you’re developing Web applications that have a lot of client-side JavaScript, you’ve already asked yourself “how the hell do I test this thing?”
To test client-side JavaScript — how it responds to user actions and what changes it makes — you’re going to have to run it in a browser. There are tools that let you do that, I tried a few and they were so bad I considered a career change.
It turns out, though, that you can test a lot of stuff by having a good enough approximation of a browser. You need a reliable HTML parser, a DOM implementation that can manage the document tree in memory, and a JavaScript engine to run the client-side code. A browser that doesn’t render anything: a headless browser.[1]
To run such a headless browser you’re going to need an environment that can load, parse and execute JavaScript real fast. Speed is a feature because there’s no bigger waste of time than waiting for your tests to finish.[2] You’ll also need a DOM implementation you can easily hack, so you can add new features (say, HTML5 Web Storage) and remove features (say, HTML5 Web Storage, because we’re testing compatibility with older browsers).
That got me thinking, “why not Node.js?” Node is built on Google Chrome’s V8 engine, which is insanely fast and optimized for loading, parsing and executing JavaScript. A perfect fit.
What about the DOM? Fortunately, much of that work was already done by Elijah Insua who created JSDOM, a DOM Level 3 implementation, and Aria Stewart who wrote the HTML5 parser.[3] I then set to build a browser environment on top of that.
Why Zombie.js? If you’re going to write an insanely fast, headless, browser, how can you not call it Zombie?
Zombie Action
Let’s see the Zombie in action. We’re going to start by setting up a new browser. The Browser object maintains state between page requests, things like cookies, Web Storage, history and (when implemented) caching. We’re typically going to use one browser instance for each session (test scenario):
1 2 | var zombie = require("zombie"); var browser = new zombie.Browser; |
Next we’re going to point the browser at the Web page we want to test and wait for stuff to happen.
Much of what happens as the browser loads the page is asynchronous: getting the HTTP response, grabbing JavaScript files, running jQuery.onready, firing timers, making AJAX requests for additional contents, etc. We also have to anticipate that our request might error.
Let’s see what that looks like:
3 4 5 6 7 8 9 | browser.on("error", function(err) { console.log("Error:", err); }); browser.on("loaded", function() { console.log("Loaded:", browser.html()); }); browser.window.location = "http://localhost:3000"; |
This example illustrates three things you’ll see often when working with Node:
- Nothing blocks, everything happens asynchronously, which is why you don’t wait for things to happen, you register callbacks.
- The Browser object is an EventEmitter that can fire different events to multiple listeners. Many Node objects are event emitters: having a uniform API simplifies composing smaller pieces into larger applications.
- The error event is special. If you don’t catch it, Node terminates and dumps the exception stack trace. It may sound harsh, but it’s a wonderful approach to failure.
Let’s simplify that further by following another Node convention:
3 4 5 6 7 | zombie.visit("http://localhost:3000/", function (err, browser) { if (err) throw(err.message); console.log("Loaded:", browser.html()); }); |
I prefer that, higher level, API.
Now we have a single callback that handles both cases. If there’s an error, the callback is invoked with a single argument containing the error. If there’s no error, the callback is invoked with null, and sometimes and additional value (in this case, a reference to the browser). All it takes is checking the first argument to figure whether we’re handling success or failure.
Again having this convention makes it easier to compose code. For example, let’s write a test case using Vows:[4]
1 2 3 4 5 6 7 8 | "this page": { topic: function() { zombie.visit("http://localhost:8080/", this.callback); }, "should have a title": function(browser) { assert.equal(browser.text("title"), "Hi there"); } } |
The topic function is a combination setup/subject, but instead of returning a value, it wires the Vows callback to the browser visit method. If there’s an error, Vows will catch and report it. Otherwise, it passes the second callback argument (here, browser) to the test case.
Clean, simple and easy to use.
Getting Smarter
1 2 3 4 5 6 7 8 | // Fill email, password and submit form browser. fill("email", "zombie@underworld.dead"). fill("password", "eat-the-living"). pressButton("Sign Me Up!", function(err, browser) { // Form submitted, new page loaded. assert.equal(browser.text("title"), "Welcome to Brains Depot"); }); |
This portion of the Zombie API is influenced by Webrat. Chances are you’re familiar with Webrat or one of the many APIs that follow this pattern. As much as possible, Zombie.js tries to behave like tools you’re already know.
Once we get the response back, we’ll want to inspect the page and make sure it has the right contents. We do this after any client-side JavaScript modifications. Since Zombie.js is built on a DOM, you can use any of the DOM methods, for example:
1 2 3 4 | item = document.getElementById("item-32"); firstLink = document.links[0].href; field = document.querySelector(":input[name=email]"); rows = table.querySelectorAll("tr:even"); |
The last two statements use CSS selectors. If you worked with jQuery before, you’ve practiced using CSS selectors to locate HTML elements.[5] In fact, Zombie uses Sizzle.js, the selector engine that powers jQuery. Like I said, tools you already know.
There’s a lot more to the Zombie API. You can check boxes, click links, fire DOM events, manage cookies and Web storage, play with the system clock, trace requests and more will be coming.
Is It Fast?
Let’s find out. I’ll use a test case I wrote for Rack OAuth 2 Server. We’re actually going to test a Ruby application.[6]
Fire up the practice server, a small Sinatra app that listens on port 8080:
1 | $ oauth2-server practice --db test |
The test case starts by loading the admin console. Since we’re not signed in, we’re redirected to the authorization page. The practice server has a simple authorization page with two buttons, “grant” and “deny”, and we’re going to click on “grant”. That will redirect us back to the admin console, with an access token that we’re going to store in localStorage. Finally, we’re going to verify that the admin consoles displays a list of client applications (see screenshot).
That probably doesn’t sound like much, but let’s see what’s really going on behind the scenes.
The admin console is a client-side application. It’s a bare HTML page, which loads jQuery, Sammy.js, Underscore.js, Protovis, a bunch of Sammy plugins, and eventually application.js. Once loaded, it changes the location hash to “#/” (the Zombie supports hashchange events), which triggers a Sammy route, which runs some application code that makes an XHR request to the server, retrieves the client list (as JSON), makes another request to load a jQuery template, and finally uses the template to render the data as HTML.
Phfew.
Looking at the Sinatra log I counted 33 HTTP requests from that test.
On my 2.4GHz ’09 MacBook Pro, the entire test runs in just under 0.9 seconds. That includes loading Node, Zombie and its dependencies, compiling the test code from CoffeeScript to JavaScript, hammering Sinatra and dumping output to the console. Because that’s how we run tests in the real world:
1 2 3 4 5 | $ time vows spec/admin-spec.coffee --spec real 0m0.909s user 0m0.763s sys 0m0.077s |
Fast enough for you?
Maybe you noticed that every changelog entry records the number of tests in the Zombie.js test suite, and how long it took to run. That’s how we’ll know if it gets any slower.
So there you are, ready to get started with Zombie.js. Have fun and may your test suite always run fast.
—-
[1] I explained the headless part. As for the full-stack: Zombie.js makes HTTP requests to your test server, exercising every layer of the stack, from HTTP header parsing to database access to response rendering.
[2] I don’t know about you, but if the tests take too long to run, I lose the flow. The speed is not just to spend less time waiting, but to stay in the flow.
[3] Initially looked as Envjs, but it needed a lot of work to fit into the asynchronous model of Node.
[4] I use Zombie in combination with Vows, an asynchronous BDD framework. The “asynchronous” part sounds like buzzword bingo, right until you start using Vows and realize a) it fits your mental model when writing the code you’re about to test, and b) it uses the common idioms and that make test writing easier.
[5] If you worked with Rails, you’ll know assert_select and how to use CSS selectors to test rendered HTML. I released it over 4 years ago, so consider Zombie.js your new & improved, faster & smarter assert_select.
[6] Zombie.js is not just for Node.js, in fact my first two uses cases are for testing Rails and Sinatra applications.

JavaScript Magazine Blog for JSMag » Blog Archive » News roundup: Zombie.js, evil.js, and even more JavaScript game engines
Spotlight – Recommend Tools For JavaScript Developers: Issue 1
The Morning Brew – Chris Alcock » The Morning Brew #762