home / blog

Jasmine 2.0 + RequireJS + Blanket + Backbone

Jasmine 2.0 provides a JavaScript unit test framework, RequireJS provides a dependency modelling/loading and Blanket provides native in-browser JavaScript code coverage – no need for a coverage server etc. So why not use all three together, well, it’s kind of tricky and kept me busy all evening.

I’m going to provide a small but scalable standalone example rather than getting bogged down in the details of my current project

Directory structure

First job is to setup the project structure. There are many possible approaches. I think the most important things are – have both development and minified third party libraries, keep third party libraries in a separate tree and have the same “package” structure for both code and tests.

.
./js
./js/main.js   <== Entry point
./js/app.js    <== Application main class
./js/config.js <== RequireJS config
./js/model     <== Application code
./js/model/bar.js
./js/view
./js/view/foo-view.js
./templates    <== Underscore templates
./templates/foo-view.html
./js/third-party
./js/third-party/underscore-min.map
./js/third-party/jquery-2.1.0.js
./js/third-party/backbone-1.1.0.js
./js/third-party/require-2.1.11.js
./js/third-party/require-2.1.11.min.js
./js/third-party/backbone-1.1.0.min.js
./js/third-party/jasmine-blanket-1.1.5.js
./js/third-party/underscore-1.6.0.min.js
./js/third-party/blanket-1.1.5.js
./js/third-party/blanket-1.1.5.min.js
./js/third-party/backbone-min.map
./js/third-party/jquery-2.1.0.min.js
./js/third-party/underscore-1.6.0.js
./js/text.js   <== RequireJS plugin for templates. Needs to be at the top level
./test
./test/spec
./test/spec/model
./test/spec/model/bar-test.js
./test/spec/view
./test/spec/view/foo-view-test.js
./test/spec-runner.js
./test/jasmine-2.0.0  <== Jasmine 2.0.0 release
./test/jasmine-2.0.0/jasmine.js
./test/jasmine-2.0.0/boot.js
./test/jasmine-2.0.0/console.js
./test/jasmine-2.0.0/jasmine_favicon.png
./test/jasmine-2.0.0/jasmine-html.js
./test/jasmine-2.0.0/jasmine.css
./css
./img
./spec-runner.html   
./index.html

RequireJS configuration

I placed the RequireJS configuration in a single file to allow it to be reused between the main application page and the jasmine tests. This would also allow reuse between multiple pages if you needed.

define([], function() {
  requirejs.config({
    baseUrl : 'js',
    paths : {
      jquery : 'third-party/jquery-2.1.0.min',
      underscore : 'third-party/underscore-1.6.0.min',
      backbone : 'third-party/backbone-1.1.0.min',
      templates : '../templates'
    },
    shim : {
      backbone : {
        deps : [ 'underscore', 'jquery' ],
        exports : 'Backbone'
      },
      underscore : {
        exports : '_'
      }
    }
  });
});

Application entry point

The application entry point main.js has a require on the config.js file and then initializes the application class. Note the use of nested requires. If you’re building with node.js you’ll need to use the findNestedDependencies = true option.

requirejs.config({
  baseUrl : 'js',
});

require([ 'config' ], function() {
  require([ 'app', 'jquery'], function(App, $) {
    $(document).ready(function() {
      new App().initialize();
    });
  });
});

HTML page

The main HTML is very simple, just a script with src of require.js with the data-main attribute pointing to the entry point file.

<!DOCTYPE html>
<html>
<head>
<title>Jasmine 2.0 + RequireJS + Blanket example</title>
<meta charset="UTF-8" />
<script data-main="js/main" src="js/third-party/require-2.1.11.min.js"></script>
</head>
<body>

</body>
</html>

Spec runner – entry point JavaScript

The jasmine spec runner uses the same config.js as the main application, this reduces any copy-and-paste and ensures the tests run with the same set of third-party libraries as the main application.

Note again the nested require() as with the main entry point.

requirejs.config({
  baseUrl : 'js'
});
require([ 'config' ], function() {
  requirejs.config( ... );
  require([ 'jquery', 'jasmine-boot', 'jasmine-blanket' ], function($, jasmine, blanket) {
    ....
  });
});

The spec runner then adds more test specific configuration to requirejs, this does not overwrite the requirejs.config() call in config.js, it simply merges with it.

  requirejs.config({
    paths : {
      'jasmine' : '../test/jasmine-2.0.0/jasmine',
      'jasmine-html' : '../test/jasmine-2.0.0/jasmine-html',
      'jasmine-boot' : '../test/jasmine-2.0.0/boot',
      'spec' : '../test/spec',
      'blanket' : 'third-party/blanket-1.1.5.min',
      'jasmine-blanket' : 'third-party/jasmine-blanket-1.1.5',
    },

Jasmine 2.0 uses a bootstrap file called boot.js. Note the shim config to setup the correct dependencies.

    shim : {
      'jasmine-boot' : {
        deps : [ 'jasmine', 'jasmine-html' ],
        exports : 'jasmine'
      },
      'jasmine-html' : {
        deps : [ 'jasmine' ]
      },
      'jasmine-blanket' : {
        deps : [ 'jasmine-boot', 'blanket' ],
        exports : 'blanket'
      }
    }

Setting up blanket is usually straightforward using attributes data-cover and data-cover-only on the script tags. However since we’re loading all our dependencies via requirejs we don’t have that option. Instead we configure blanket programatically using blanket.config() calls.

The blanket-jasmine adapter at time of writing does not quite support jasmine 2.0 due to some API changes. I used the patch from stackoverflow.com/questions/21420291/blanketjs-jasmine-2-0-not-working to fix it up.

  require([ 'jquery', 'jasmine-boot', 'jasmine-blanket' ], function($, jasmine, blanket) {

    // blanket.options('debug', true);

    // include filter
    blanket.options('filter', 'js/');
    // exclude filter
    blanket.options('antifilter', [ 'js/third-party', '../test/spec/', 'js/text.js' ]);
    blanket.options('branchTracking', true); 

Another funny issue with jasmine 2.0 is the use of the window.onload callback in boot.js to kick the tests off. Therefore we call it manually, wrapped with a jQuery document ready callback to ensure the DOM is loaded.

Loading the individual spec js files is easy, we just push them into an array and require() that first

    var specs = [];

    specs.push('spec/view/foo-view-test');
    specs.push('spec/model/bar-test');

    $(document).ready(function() {
      require(specs, function(spec) {
        window.onload();
      });
    });

Putting it all together…

requirejs.config({
  baseUrl : 'js'
});
require([ 'config' ], function() {

  requirejs.config({
    paths : {
      'jasmine' : '../test/jasmine-2.0.0/jasmine',
      'jasmine-html' : '../test/jasmine-2.0.0/jasmine-html',
      'jasmine-boot' : '../test/jasmine-2.0.0/boot',
      'spec' : '../test/spec',
      'blanket' : 'third-party/blanket-1.1.5.min',
      'jasmine-blanket' : 'third-party/jasmine-blanket-1.1.5',
    },
    shim : {
      'jasmine-boot' : {
        deps : [ 'jasmine', 'jasmine-html' ],
        exports : 'jasmine'
      },
      'jasmine-html' : {
        deps : [ 'jasmine' ]
      },
      'jasmine-blanket' : {
        deps : [ 'jasmine-boot', 'blanket' ],
        exports : 'blanket'
      }
    }
  });

  require([ 'jquery', 'jasmine-boot', 'jasmine-blanket' ], function($, jasmine, blanket) {

    // blanket.options('debug', true);

    // include filter
    blanket.options('filter', 'js/');
    // exclude filter
    blanket.options('antifilter', [ 'js/third-party', '../test/spec/', 'js/text.js' ]);
    blanket.options('branchTracking', true); 

    var jasmineEnv = jasmine.getEnv();
    jasmineEnv.addReporter(new jasmine.BlanketReporter());

    jasmineEnv.updateInterval = 1000;

    var specs = [];

    specs.push('spec/view/foo-view-test');
    specs.push('spec/model/bar-test');

    $(document).ready(function() {
      require(specs, function(spec) {
        window.onload();
      });
    });
  });
});

Spec runner – HTML

This is simple, just require.js and a pointer to the spec runner entry point

<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Jasmine Spec Runner v2.0.0</title>

<link rel="shortcut icon" type="image/png"
	href="test/jasmine-2.0.0/jasmine_favicon.png">

<link rel="stylesheet" type="text/css"
	href="test/jasmine-2.0.0/jasmine.css">

<script data-main="test/spec-runner"
	src="js/third-party/require-2.1.11.min.js"></script>

</head>

<body>
</body>
</html>

The tests

The tests each use require() to load the classes they wish to test.

define([ 'model/bar' ], function(Bar) {
  describe("data access", function() {
    var model = {};
    beforeEach(function() {
      model = new Bar([ {
        type : 'cow',
        size : 1
      }, {
        type : 'horse',
        size : 2
      } ]);
    });
    it("can search by type", function() {
      expect(model.findByType('horse')[0].get('size')).toBe(2);
    });
  });
});
This entry was posted in geek and tagged , , , . Bookmark the permalink.

Leave a Reply

Your email address will not be published.