Testing with Node, Jasmine, and Require.JS, part II

With some serious interface revisions headed down the pipeline, last week’s dive into the realm of node testing is headed to the next level. I previously sketched a rough framework for server-side AMD testing based on require.js, jasmine, and coffeescript; now, it’s time to add in support for limited testing of the application’s client-side elements. For the interface in question, that functionality is primarily wrapped up in two familiar libraries: jQuery and Backbone.js.

That’s no browser instance!

I’ll be the last to argue that running client-side tests outside of a browser environment is anything short of presumptuous. But testing at least basic functionality before running a build can also help ensure that browser issues will be caught as early as possible.

The obvious challenge is to set up a browser-like environment without a browser. For basic tests, that mostly means making sure that any required variables are available in the global namespace (global, to the server; window, in browser parlance). In node, this can be done using jsdom and jquery packages:

$ npm install jsdom
$ npm install jquery

The test environment can now be adjusted to add objects representing window.jQuery and window.document to the global namespace. Holding my nose against the reliance on global variables—it’s just a hack—I’ve created a runner in my helper like so:

// in spec_runner.js
global.initDOM = function () {

  jsdom = require('jsdom');
  jQuery = require('jquery').create();
  global.jQuery = global.$ = jQuery;

  window = jsdom.jsdom().createWindow('')
  global.document = window.document;

  global.addEventListener = window.addEventListener
}

Following en Jasmine vogue, this function might better belong within a spec_helper.* file, but a patch inside the runner will work for now.

Have a Backbone

The second trick is to make sure that Backbone lands on its feed. Inside a browser, Backbone identifies a suitable DOM-manipulation library (jQuery, Zepto, or whatever ender’s got, in that order) to bind to its internal $ variable. If the library isn’t available when Backbone is initialized, it will need to be bound later. This can be done using the Backbone.setDomLibrary method.

Backbone.setDomLibrary(jQuery);

From here, it’s cake: the Backbone-enabled helper will simply need to create a faux-DOM and add itself to the global namespace:

// in spec_runner.js
global.initBackbone = function () {
  global.initDOM();
  global.Backbone = require('backbone');
  global.Backbone.setDomLibrary(jQuery);
}

Writing tests

Now, Backbone can be enabled within unit tests by calling the global initBackbone method. Once this is done, DOM-dependent methods (think Backbone Views) may be tested in an environment that vaguely resembles a standards-compliant browser:

# in specs/view.spec.coffee
define ['src/View'], (View) ->

  initBackbone()

  describe 'View', ->
    it 'Does things', ->
      # ... expects(), etc

Finally,

Besides adding functions to create a faux-DOM and set up Backbone, I’ve also reverted to the default jasmine-node package and streamlined the way its methods are included in the global namespace. I can’t help feeling a little dirty at the number of globals going by, but hey—the window namespace that global is being asked to emulate has never been accused of excessive tidiness.

// in spec_runner.js
jasmine = require('jasmine-node');

// map jasmine methods to global namespace
for (key in jasmine) {
    if (jasmine[key] instanceof Function) {
    global[key] = jasmine[key];
  }
}

All of the changes are wrapped up on github, where I’ve pushed a demo repository containing the basic testing setup described here.

Let’s keep in touch

Reach out on Twitter or subscribe for (very) occasional updates.

Hey, I'm RJ: digital entomologist and intermittent micropoet, writing from the beautiful Rose City.