If you’re a TDD addict you know that it’s not always easy to Test Drive your JavaScript. Which testing framework should you use? How do you set your CI pipeline up? Etc…
There are quite a few frameworks out there and it seems like writing a testing framework is what everybody wants to do. I am pretty sure we don’t need any more. We just need them to be easy to use. In this post I’ll show you how incredibly easy it is these days to start using Grunt, Jasmine and PhantomJS.
What You’ll Need
node.js – get the latest version (http://nodejs.org/download/ or a package manager of your choice). At the time of this writing the latest stable version is 0.10.2
daniel at daniel in /tmp $ node -v v0.10.2 daniel at daniel in /tmp $ npm -v 1.2.15
That’s it. Just follow along.
Grunt
What makes this previously tedious and sometimes difficult task easy is Grunt. Grunt is a task based command line tool for JavaScript projects. To run Grunt you first install the command line interface (grunt-cli) globally.
$ npm install -g grunt-cli ... /usr/local/bin/grunt -> /usr/local/lib/node_modules/grunt-cli/bin/grunt grunt-cli@0.1.6 /usr/local/lib/node_modules/grunt-cli ├── nopt@1.0.10 (abbrev@1.0.4) └── findup-sync@0.1.2 (lodash@1.0.1, glob@3.1.21) daniel at daniel in /tmp $
The Grunt command line interface is a thin wrapper that understands how to find the Grunt version your project really needs.
Create a package.json for dependencies
An easy way to bootstrap a package.json is to use npm init.
daniel at daniel in /tmp $ mkdir /tmp/jasmine-grunt && cd /tmp/jasmine-grunt daniel at daniel in /tmp/jasmine-grunt $ npm init ... (accept the defaults) Is this ok? (yes) npm WARN package.json jasmine-grunt@0.0.0 No README.md file found! daniel at daniel in /tmp/jasmine-grunt $
Install local grunt
$ npm install grunt --save-dev ... grunt@0.4.1 node_modules/grunt ... └── js-yaml@2.0.3 (argparse@0.1.12) daniel at daniel in /tmp/jasmine-grunt $
Install grunt-contrib-jasmine
$ npm install grunt-contrib-jasmine --save-dev ... > phantomjs@0.2.6 install /private/tmp/jasmine-grunt/node_modules/grunt-contrib-jasmine/node_modules/grunt-lib-phantomjs/node_modules/phantomjs > node install.js Requesting /private/tmp/jasmine-grunt/node_modules/grunt-contrib-jasmine/node_modules/grunt-lib-phantomjs/node_modules/phantomjs/tmp/phantomjs-1.7.0-macosx.zip Receiving... Recieved 782K... ... Recieved 10182K total. Extracting zip contents Read stream closed Renaming extracted folder phantomjs-1.7.0-macosx -> phantom Fixing file permissions Done. Phantomjs binary available at /private/tmp/jasmine-grunt/node_modules/grunt-contrib-jasmine/node_modules/grunt-lib-phantomjs/node_modules/phantomjs/lib/phantom/bin/phantomjs grunt-contrib-jasmine@0.3.3 node_modules/grunt-contrib-jasmine ├── rimraf@2.0.3 (graceful-fs@1.1.14) └── grunt-lib-phantomjs@0.1.0 (semver@1.0.14, eventemitter2@0.4.11, temporary@0.0.5, phantomjs@0.2.6) daniel at daniel in /tmp/jasmine-grunt $
Create Gruntfile.js
Grunt needs some configuration. Let’s start with this:
module.exports = function(grunt) { 'use strict'; // Project configuration. grunt.initConfig({ jasmine : { src : 'src/**/*.js', options : { specs : 'specs/**/*.js' } } }); grunt.loadNpmTasks('grunt-contrib-jasmine'); };
Add a Spec
Create a Jasmine spec, specs/Crisper-spec.js looking like this
describe("Crisper", function() { it("should be a constructor function", function() { expect(typeof window.Crisper).toBe("function"); }); });
Run the Spec
$ grunt jasmine Running "jasmine:src" (jasmine) task Testing jasmine specs via phantom x Crisper:: should be a constructor function: failed Expected 'undefined' to be 'function'. (1) 1 spec in 0.001s. >> 1 failures Warning: Task "jasmine:src" failed. Use --force to continue. Aborted due to warnings. daniel at daniel in /tmp/jasmine-grunt $
Continuously Watch Files
Install grunt-contrib-watch:
$ npm install grunt-contrib-watch --save-dev ... grunt-contrib-watch@0.3.1 node_modules/grunt-contrib-watch └── gaze@0.3.3 (minimatch@0.2.11, fileset@0.1.5) daniel at daniel in /tmp/jasmine-grunt $
Modify your Gruntfile.js
module.exports = function(grunt) { 'use strict'; // Project configuration. grunt.initConfig({ jasmine : { src : 'src/**/*.js', options : { specs : 'specs/**/*.js' } }, watch: { files: '**/*.js', tasks: ['jasmine'] } }); grunt.loadNpmTasks('grunt-contrib-jasmine'); grunt.loadNpmTasks('grunt-contrib-watch'); };
Start watching
$ grunt watch Running "watch" task Waiting...
Add an implementation of the Crisper constructor function
Add src/Crisper.js with the following content:
(function(win) { win.Crisper = function() { }; }(window));
The watch process kicks in and then runs Jasmine with the following result:
Running "watch" task Waiting...OK gt;> File "src/Crisper.js" changed. Running "jasmine:src" (jasmine) task Testing jasmine specs via phantom . 1 spec in 0.002s. >> 0 failures Done, without errors.
Obviously the implementation’s lacking and we need more tests to drive the rest. Let’s change the spec to look like this:
describe("Crisper", function() { it("should be a constructor function taking one argument", function() { expect(typeof window.Crisper).toBe("function"); }); it("should be a constructor function taking one argument", function() { expect(window.Crisper.length).toBe(1); }); });
Jasmine returns:
>> File "specs/Crisper-spec.js" changed. Running "jasmine:src" (jasmine) task Testing jasmine specs via phantom .x Crisper:: should be a constructor function taking one argument: failed Expected 0 to be 1. (1) 2 specs in 0.002s. >> 1 failures Warning: Task "jasmine:src" failed. Use --force to continue. Aborted due to warnings.
Add a parameter “name” to the Crisper function and we have passing tests again. Finish it off by adding a test to verify that creating an Crisper instance sets the name property as expected.
Final spec:
describe("Crisper", function() { it("should be a constructor function", function() { expect(typeof window.Crisper).toBe("function"); }); it("should be a constructor function taking one argument", function() { expect(window.Crisper.length).toBe(1); }); it("instances should get a name property initialized with the constructor argument", function() { var reza = new window.Crisper('Reza'); expect(reza.name).toBe("Reza"); }); });
Final implementation:
(function(win){ win.Crisper = function(name) { this.name = name; }; }(window));
Caveats
Both grunt-contrib-jasmine and grunt-contrib-watch are moving targets and have had some issues when I’ve been using them. Especially using Mac OS X which is what I’m running in my examples above. Some gotchas and potential work-arounds:
- The watch command doesn’t pick up newly created files. This is supposedly fixed but I couldn’t get it to work. Work-around: Restart the watch after the file has been created.
- PhantomJS is not installed properly when grunt-contrib-jasmine was installed. This is apparently a downstream problem related to the grunt-lib-phantomjs and/or phantomjs packages. Workaround: Explicitly install phantomjs locally.
grunt install phantomjs –save-dev - You may need to use sudo to install the grunt-cli package
Great hands-on advice on how to get kicking.with TDD on JavaScript.
Thanks for the tutorial! One thing I’ve been trying to get my head around – I’m new to unit testing – is how to test my DOM manipulation using grunt-contrib-jasmine. If my javascript file has a function that, say, adds an element to the page, how can my spec file confirm that the function works?
Darryl, you’ve probably worked this out by now, but you can include whatever you need to access the DOM and test against it in a test runner like Jasmine. I use Mocha with jQuery to test Backbone.Marionette code, but the principle is the same.
Awesome tutorial, it really proves that theres no reason not to do TDD in frontend!
No matter what I do I can’t get any more out of it than “Testing jasmine specs via phantom”
Any ideas?