DRY Testing of Require.js Based Backbone Apps Using Jasmine

| Comments

EDIT: 2/15/2013: Scratch this way of doing it. I didn’t fully understand how requirejs worked in tests when I wrote this. You should instead just be using a define around your tests, which should work very similarly to the rest of your requirejs code under test. Like this:

define(['models/Todo', 'views/CountView'], function(Todo, View){
  describe('View :: Count Remaining Items', function() {
    var todos, view, mockData;
 
    beforeEach(function(Todo, View) {
      todos = new Todo.Collection();
      view = new View({collection: that.todos});
      mockData = { title: 'Foo Bar', timestamp: new Date().getTime() };
      $('#sandbox').html(that.view.render().el);
    });
    ...

Old (not optimal) way:

I’ve recently started a new backbone.js application that uses require.js modules to keep it organized and clean.

Doing development with modules is a strong trend in the backbone.js world. @addyosmani’s Developing Backbone.js Applications ebook recommends using requirejs and it’s also comes baked in to Backbone Boilerplate.

There are quite a few examples on how to use require.js modules with backbone, but very few that actually show how to use it in combination with tests, specifically with Jasmine, a BDD test framework that I’ve really come to like. I did find one post by Uzi Kilon that was helpful.

He gives a nice overview of the problem as well as extensive documentation on how he solves it by calling require in beforeEach:

describe('View :: Count Remaining Items', function() {
 
  beforeEach(function() {
    var flag = false,
        that = this;
 
    require(['models/Todo', 'views/CountView'], function(Todo, View) {
      that.todos = new Todo.Collection();
      that.view = new View({collection: that.todos});
      that.mockData = { title: 'Foo Bar', timestamp: new Date().getTime() };
      $('#sandbox').html(that.view.render().el);
      flag = true;
    });
 
    waitsFor(function() {
      return flag;
    });
 
  });
  ...

This strategy sets up a flag boolean variable that is only set to true once the require is satisfied and the data in beforeEach is set up. This is monitored with Jasmine’s waitsFor.

In my testing, this works great, but as the number of beforeEach methods I had started to grow, I needed to repeat this code in every one. It was bothering me and I wanted to DRY it up.

The solution I came up with was to create a new file called jasmine-require.js:

/* 
    utility global functions for jasmine, global to match existing jasmine global functions 

    It's expected that the last parameter is a function that you want to execute 
    within the context of the require all preceding parameters are passed to the require method

    The most likely way to call this is:

    waitsForRequire(['require_dep1', 'require_dep2',...], function(dep1, dep2) { 
        ...code that gets dependencies...
    }) 
*/
var waitsForRequire = function () {
  var argv = Array.prototype.slice.call(arguments),
      done = false;

  var callback = typeof _.last(argv) === 'function' ? argv.pop() : function(){};

  return function () {
    require.apply(null, argv.concat(function () {
      callback.apply(null, arguments);
      done = true;
    }));

    waitsFor(function () { return done; });
  };
};

I then modify the SpecRunner.js shim dependencies for jasmine so that jasmine-html also depends on this new file:

require.config({
  baseUrl: "/js/",
  urlArgs: 'cb=' + Math.random(),
  paths: {
    jquery: 'lib/jquery-1.8.0',
    underscore: 'lib/underscore-1.3.3',
    backbone: 'lib/backbone-0.9.2',
    'backbone.localStorage': 'lib/backbone.localStorage',
    jasmine: '../test/lib/jasmine',
    'jasmine-html': '../test/lib/jasmine-html',

    'jasmine-require': '../test/lib/jasmine-require', // ADDED!

    spec: '../test/jasmine/spec/'
  },
  shim: {
    underscore: {
      exports: "_"
    },
    backbone: {
      deps: ['underscore', 'jquery'],
      exports: 'Backbone'
    },
    'backbone.localStorage': {
      deps: ['backbone'],
      exports: 'Backbone'
    },
    jasmine: {
      exports: 'jasmine'
    },
    'jasmine-html': {

      deps: ['jasmine', 'jasmine-require'],  // ADDED jasmine-require!

      exports: 'jasmine'
    }
  }
});

Then, instead of having to repeat myself, I can DRY my beforeEach up to this:

describe('View :: Count Remaining Items', function() {
  var todos, view, mockData;
 
  beforeEach(waitsForRequire(['models/Todo', 'views/CountView'], function(Todo, View) {
      todos = new Todo.Collection();
      view = new View({collection: that.todos});
      mockData = { title: 'Foo Bar', timestamp: new Date().getTime() };
      $('#sandbox').html(that.view.render().el);
  }));
  ...

I’m not normally a fan of global JavaScript functions, but this fits with how jasmine works with having describe, it, beforeEach, etc all as global functions in tests. If you don’t like this you could instead add this in a namespace (or attach it to the jasmine object with _.extend(jasmine, { waitsForRequire: function(){…body above…}});).

Comments