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);
});
});
});