"
To grunt and sweat under a weary life"
-- Shakespeare, Hamlet
Act III, Scene 1, Line 77
Prelude
My first experience of continuous testing was using
NCrunch, it was funny how quickly this change the way I wrote software.
What is Continuous Testing
In a typical TDD style of programming you do the following.
- Write a failing test.
- Stop and run test to prove that test is failing.
- Write code to pass failing test.
- Stop and run test to prove that test is now passing (and make sure nothing is broken).
- Refactor code.
- Stop and run test to prove nothing as been broken by refactoring.
- Repeat.
Notice anything interesting? Yep, there is a whole lot of
stopping in the cycle. This stopping is one of the common complaints one hears from people just starting out in TDD (I know most of my readers are well past that point, but there are a LOT of programmers that do not do TDD, so this issue is still relevant).
How can we fix this? It would be nice if we had something running in the background which ran our test for us and would notify us of any failures as they came up, this could work like a word processor's spell check.
I remember when I first started writing on a computer using
WordPerfect (yep, I am 33). Using a word processor back in the dark days when one would start Windows from DOS, one would find themselves following a similar process with a spell checker. You would do the following.
- Write some text.
- Stop and run the spell checker.
- Fix any issues.
- Write some more.
- Stop and run the spell checker again.
- Fix any issues.
- Repeat.
This looks a like like the typical TDD session.
Continuous testing is testing in which the test are always running. It works a lot like a spell checker in a modern word processor. As you write code your test are continuously running and given you feedback on your code.
With continuous testing, TDD looks like the following.
- Write a failing test.
- Write code to pass the test.
- Refactor.
- Repeat.
Yep, coding without stopping. The first time I used NCrunch for a whole day, I noticed when I went to check my code into source control that I had not saved it yet! The way that NCrunch works it automatically builds and tests your code for you, so you do not even notice that you have not saved yet. Depending on how much you trust your computer and power supply this may or may not be a good thing, but it shows how a tool can change the way that you work.
The team that I work with have fallen in love with continuous testing so much that when NCrunch went from free to pay we took a quick poll and found that everyone would rather drop their
ReSharper licence in order to get a NCrunch licence if push-came-to-shove (ReSharper is a great tool and I would not want to do .Net programming without it, in fact I have a ReSharper T-shirt on as I am typing this (do not judge me), but continuous testing is that important). Luckily for us, we did not have to follow through with that choice and are now enjoying NCrunch along with ReSharper.
How to Setup Grunt
Enough about NCrunch this post is actually about how to set up a continuous testing environment for
Node.js using
Grunt and
Mocha. First thing first, you'll need to have
Node.js installed if you do not already have it.
Assuming that you have Node.js installed let's look at getting
Grunt setup.
The easiest way to install Grunt is via npm (Node Package Manager), fire up the console.
C:\kata>npm install -g grunt-cli
This will install the
Grunt Command Line Interface and will add grunt to your system path.
Adding Grunt to Your Projecting
Now that we have the Grunt Command Line Interface install we can add Grunt to our Node.js code.
Let's create a new Grunt Hello World project.
C:\kata>mkdir gruntHelloWorld
C:\kata>cd gruntHelloWorld
C:\kata\gruntHelloWorld>mkdir src
C:\kata\gruntHelloWorld>mkdir test
We now have a directory called
gruntHelloWorld which has a
src folder for our source code and a
test folder for our tests, feel free to code these what you want you'll just have to modify the instructions a bit from here on out.
We are now ready to create a
package.json file to hold our Node project dependencies (you can skip this if you want).
npm init will give you a prompt like the following.
C:\kata\gruntHelloWorld>npm init
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sane defaults.
See `npm help json` for definitive documentation on these fields
and exactly what they do.
Use `npm install --save` afterwards to install a package and
save it as a dependency in the package.json file.
Press ^C at any time to quit.
name: (gruntHelloWorld)
Follow the prompt (do not worry you can fix any issues you have later). Here is what I used incase you want to copy. Note, for the name you cannot have spaces. Also note that you can hit enter to just use the default values.
name: (gruntHelloWorld) HelloWorld
version: (0.0.0) 0.0.1
description: A simple example of continuous testing using Grunt and Mocha.
entry point: (index.js) hello.js
test command: grunt
git repository:
keywords: example grunt mocha hello world
author: Mike Harris
license: (BSD-2-Clause)
About to write to C:\kata\gruntHelloWorld\package.json:
{
"name": "HelloWorld",
"version": "0.0.1",
"description": "A simple example of continuous testing using Grunt and Mocha.",
"main": "hello.js",
"directories": {
"test": "test"
},
"scripts": {
"test": "grunt"
},
"keywords": [
"example",
"grunt",
"mocha",
"hello",
"world"
],
"author": "Mike Harris",
"license": "BSD-2-Clause"
}
Is this ok? (yes) y
C:\kata\gruntHelloWorld>
What did we just do? We create a file which will hold all of our dependencies for our Node project, this file is called package.json.
C:\kata\gruntHelloWorld>type package.json
{
"name": "HelloWorld",
"version": "0.0.1",
"description": "A simple example of continuous testing using Grunt and Mocha.",
"main": "hello.js",
"directories": {
"test": "test"
},
"scripts": {
"test": "grunt"
},
"keywords": [
"example",
"grunt",
"mocha",
"hello",
"world"
],
"author": "Mike Harris",
"license": "BSD-2-Clause"
}
Let's add Grunt to this project.
C:\kata\gruntHelloWorld>npm install grunt --save-dev
Notice the --save-dev on the npm command, this will add a dependency on grunt to our package.json file, which will allow us to our project dependencies with others.
C:\kata\gruntHelloWorld>type package.json
{
"name": "HelloWorld",
"version": "0.0.1",
"description": "A simple example of continuous testing using Grunt and Mocha.",
"main": "hello.js",
"directories": {
"test": "test"
},
"scripts": {
"test": "grunt"
},
"keywords": [
"example",
"grunt",
"mocha",
"hello",
"world"
],
"author": "Mike Harris",
"license": "BSD-2-Clause",
"devDependencies": {
"grunt": "~0.4.2"
}
}
Now that we have Grunt setup, we need to create the Gruntfile.js file.
Adding in the Gruntfile.js
The
Gruntfile.js tells Grunt what to do, think of it as an
Apache Ant build file for Grunt, but instead of being stored in XML and limited to just building, it is written in JavaScript and is used for tasks (which can include "building").
Create a new file called
Gruntfile.js at the root of the project (right next to
package.json), I will use the text editor
Sublime Text to do this (since it is current favorite text editor, but
vim or
notepad or ... would work just as well).
Our Gruntfile.js will need to export a function which takes a single argument.
C:\kata\gruntHelloWorld>type Gruntfile.js
module.exports = function(grunt) {
// add task here
};
Now that we have our wrapper function setup we can add tasks to it.
Adding in Mocha
First let's get the latest version of
Mocha from npm.
C:\kata\gruntHelloWorld>npm install mocha --save-dev
Now we will the Grunt plugin
grunt-mocha-test to run our Mocha tests.
C:\kata\gruntHelloWorld>npm install grunt-mocha-test --save-dev
Now we'll initialize the configuration of Grunt by adding in a method called
grunt.initConfig (which is an alias for
grunt.config.init) to the function we are exporting.
module.exports = function(grunt){
grunt.initConfig({
mochaTest: {
test: {
options: {
reporter: 'spec',
clearRequireCache: true
},
src: ['test/*.js']
},
}
});
grunt.loadNpmTasks('grunt-mocha-test');
grunt.registerTask('default', ['mochaTest']);
};
We call
grunt.initConfig with an object containing a name property of
mochaTest (you should be able to call it whatever you want), this property has an object with a property of
test which in turn has
options which is where we place our options to Mocha. We set the
reporter to use spec and to clear the require cache (I find this makes life a lot easier). Next we load the npm tasks (this could have been done earlier) and lastly we register
mochaTest as part of default to Grunt.
C:\kata\gruntHelloWorld>grunt
If we want we could register a task just for testing.
module.exports = function(grunt){
grunt.initConfig({
mochaTest: {
test: {
options: {
reporter: 'spec',
clearRequireCache: true
},
src: ['test/*.js']
},
}
});
grunt.loadNpmTasks('grunt-mocha-test');
grunt.registerTask('default', ['mochaTest']);
grunt.registerTask('test', ['mochaTest']);
};
Now we can call grunt with test and it will execute
mochaTest task.
C:\kata\gruntHelloWorld>grunt test
Let's look at adding in continuous testing.
Adding in Watch
We'll now add a watcher which will call our
mochaTest task on any changes to our files.
We'll add the plugin
grunt-contrib-watch.
C:\kata\gruntHelloWorld>npm install grunt-contrib-watch --save-dev
We now add watch to our Gruntfile.js
module.exports = function(grunt){
grunt.initConfig({
mochaTest: {
test: {
options: {
reporter: 'spec',
clearRequireCache: true
},
src: ['test/*.js']
},
},
watch: {
js: {
options: {
spawn: true,
interrupt: true,
debounceDelay: 250,
},
files: ['Gruntfile.js', 'src/*.js', 'test/*.js'],
tasks: ['mochaTest']
}
}
});
grunt.loadNpmTasks('grunt-mocha-test');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.registerTask('default', ['mochaTest']);
grunt.registerTask('test', ['mochaTest']);
};
I find these
options work best for me, but you may want to read the
documentation to find what will work best for you.
The
files property is where we say what files to watch in this case the
Gruntfile.js and anything under our
src and
test folder which is a JavaScript file.
The
task property says which task to run when the watch detects a change in the files it is watching, in this case we want to run our Mocha tests (and thus create a continuous testing environment).
Start up the watcher with
grunt watch.
C:\kata\gruntHelloWorld>grunt watch
We are now ready for some continuous testing TDD goodness.
Hello World
Let's add in expect.js for testing our code (feel free to use whatever you prefer).
C:\kata\gruntHelloWorld>npm install expect.js --save-dev
(If you have been following along you'll need to Ctrl+c out of the watch first.)
We'll start off with the following in our newly created
helloSpec.js file.
var sut = require("../src/hello"),
expect = require("expect.js");
describe("Hello World using TDD", function(){
it("Given nothing then Hello is returned", function(){
expect(sut.helloer()).to.be.equal("Hello");
});
});
Running the test we'll have something like the following.
C:\kata\gruntHelloWorld>grunt test
We can now create the
hello.js file with the following to pass the test.
exports.helloer = function(){
return "Hello";
}
We now can start the watch up and do some TDD.
C:\kata\gruntHelloWorld>grunt watch
Boom! We now have a continuous testing environment set up!
We can now set up a failing test around adding in a name to say hello to.
We add the following test to
helloSpec.js in our test folder and watch the watch detect the change and report the failure.
var sut = require("../src/hello"),
expect = require("expect.js");
describe("Hello World using TDD", function(){
it("Given nothing then Hello is returned", function(){
expect(sut.helloer()).to.be.equal("Hello");
}),
it("Given Mike then Hello Mike is returned", function(){
expect(sut.helloer("Mike")).to.be.equal("Hello Mike");
});
});
Nice! Now we can make it pass (this is what I've added to
hello.js).
exports.helloer = function(name){
var ret = "Hello";
if (name) ret += " " + name;
return ret;
}
Now we have a fully operational continuous testing environment.
Allude
Here is what we got now in terms of files.
package.json
{
"name": "HelloWorld",
"version": "0.0.1",
"description": "A simple example of continuous testing using Grunt and Mocha.",
"main": "hello.js",
"directories": {
"test": "test"
},
"scripts": {
"test": "grunt"
},
"keywords": [
"example",
"grunt",
"mocha",
"hello",
"world"
],
"author": "Mike Harris",
"license": "BSD-2-Clause",
"devDependencies": {
"grunt": "~0.4.2",
"mocha": "~1.17.1",
"grunt-mocha-test": "~0.9.0",
"grunt-contrib-watch": "~0.5.3",
"expect.js": "~0.2.0"
}
}
Gruntfile.js
module.exports = function(grunt){
grunt.initConfig({
mochaTest: {
test: {
options: {
reporter: 'spec',
clearRequireCache: true
},
src: ['test/*.js']
},
},
watch: {
js: {
options: {
spawn: true,
interrupt: true,
debounceDelay: 250,
},
files: ['Gruntfile.js', 'src/*.js', 'test/*.js'],
tasks: ['mochaTest']
}
}
});
grunt.loadNpmTasks('grunt-mocha-test');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.registerTask('default', ['mochaTest']);
grunt.registerTask('test', ['mochaTest']);
};
src\hello.js
exports.helloer = function(name){
var ret = "Hello";
if (name) ret += " " + name;
return ret;
}
test\helloSpec.js
var sut = require("../src/hello"),
expect = require("expect.js");
describe("Hello World using TDD", function(){
it("Given nothing then Hello is returned", function(){
expect(sut.helloer()).to.be.equal("Hello");
}),
it("Given Mike then Hello Mike is returned", function(){
expect(sut.helloer("Mike")).to.be.equal("Hello Mike");
});
});