Test-driven Node.js Development
The code examples in The Node Beginner Book only described a toy project, and we came away with not writing any tests for it. If writing tests is new for you, and you have not yet worked on software in a test-driven manner, then I invite you to follow along and give it a try.
We need to decide on a test framework that we will use to implement our tests. A lack of choice is not an issue in the JavaScript and Node.js world, as there are dozens of frameworks available. Personally, I prefer Jasmine, and will therefore use it for my examples.
Jasmine is a framework that follows the philosophy of behaviour-driven development, which is kind of a subculture within the community of test-driven developers. This topic alone could easily fill its own book, thus I'll give only a brief introduction.
The idea is to begin development of a new software unit with its specification, followed by its implementation (which, by definition, must satisfy the specification).
Let's make up a real world example: we order a table from a carpenter. We do so by specifying the end result:
I need a table with a top that is 6 x 3 ft. The height of the top must be adjustable—2.5 to 4.0 ft. I want to be able to adjust the top's height without standing up from my chair. I want the table to be black, and cleaning it with a wet cloth should be possible without damaging the material. My budget is $500.
Such a specification allows to share a goal between us and the carpenter. We don't have to care for how exactly the carpenter will achieve this goal. As long as the delivered product fits our specification, both of us can agree that the goal has been reached.
With a test-driven or behaviour-driven approach, this idea is applied to building software. You wouldn't build a piece of software and then define what it's supposed to do. You need to know in advance what you expect a unit of software to do. Instead of doing this vaguely and implicitly, a test-driven approach asks you to do the specification exactly and explicitly. Because we work on software, our specification can be software, too: we only need to write functions that check if our unit does what it is expected to do. These check functions are unit tests.
Let's create a software unit which is covered by tests that describe its expected behaviour. In order to actually drive the creation of the code with the tests, let's write the tests first. We then have a clearly defined goal: making the tests pass by implementing code that fulfills the expected behaviour, and nothing else.
In order to do so, we create a new Node.js project with two folders in it:
src/ spec/
The spec folder is where our test cases go – in Jasmine lingo, these are called specifications, hence spec. The spec folder mirrors the file structure under src, that is, a source file at src/foo.js is mirrored by a specification at spec/fooSpec.js.
Following the tradition, we will test and implement a Hello World code unit. Its expected behaviour is to return a string Hello Joe! if called with Joe as its first and only parameter. This behaviour can be specified by writing a unit test.
To do so, we create a file spec/greetSpec.js, with the following content:
'use strict';
var greet = require('../src/greet');
describe('greet', function() {
it('should greet the given name', function() {
expect(greet('Joe')).toEqual('Hello Joe!');
});
it('should greet no-one special if no name is given', function() {
expect(greet()).toEqual('Hello world!');
});
});
This is a simple, yet complete specification. It is a programmatical description of the behaviour we expect from a yet-to-be-written function named greet.
The specification says that if the function greet is called with Joe as its first and only parameter, the return value of the function call should be the string Hello Joe!. If we don't supply a name, the greeting should be generic.
As you can see, Jasmine specifications have a two-level structure. The top level of this structure is a describe block, which consists of one or more it blocks.
An it block describes a single expected behaviour of a single unit under test, and a describe block summarizes one or more blocks of expected behaviours, therefore completely specifying all expected behaviours of a unit.
Let's illustrate this with a real-world unit described by a Jasmine specification:
describe( 'A candle' , function() {
it( 'should burn when lighted' , function() {
// ...
});
it( 'should grow smaller while burning' , function() {
// ...
});
it( 'should no longer burn when all wax has been burned', function(){
// ...
});
it( 'should go out when no oxygen is
available to it' , function() {
// ...
});
});
As you can see, a Jasmine specification gives us a structure to fully describe how a given unit should behave.
Not only can we describe expected behaviour, we can also verify it. This can be done by running the test cases in the specification against the actual implementation.
After all, our Jasmine specification is just another piece of JavaScript code which can be executed. The NPM package jasmine-node ships with a test case runner which allows us to execute the test case, with the added benefit of a nice progress and result output.
Let's create a package.json file that defines jasmine-node as a dependency of our application – then we can start running the test cases of our specification.
As described earlier, we need to place the package.json file at the topmost folder of our project. Its content should be as follows:
{ "devDependencies": { "jasmine-node": "^1.14.5" } }
We talked about the dependencies section of package.json before – but here we declare jasmine-node in a devDependencies block. The result is basically the same: NPM knows about this dependency and installs the package and its dependencies for us. However, dev dependencies are not needed to run our application – as the name suggests, they are only needed during development.
NPM allows you to skip dev dependencies when deploying applications to a production system – we will get to this later.
In order to have NPM install jasmine-node, please run the npm install command in the top folder of your project.
We are now ready to test our application against its specification.
Of course, our greet function cannot fulfill its specification yet, simply because we have not yet implemented it. Let's see how this looks by running the test cases. From the root folder of our new project, execute the following:
./node_modules/jasmine-node/bin/jasmine-node spec/greetSpec.js
As you can see, Jasmine isn't too happy with the results yet. We refer to a Node module in src/greet.js, a file that doesn't even exist, which is why Jasmine bails out before even starting the tests:
Exception loading: spec/greetSpec.js { [Error: Cannot find module '../src/greet'] code: 'MODULE_NOT_FOUND' }
Well, let's create the module, in file src/greet.js:
'use strict' ;
var greet = function() {};
module.exports = greet;
Now we have a general infrastructure, but of course we do not yet behave as the specification wishes. Let's run the test cases again:
FF Failures: 1) greet should greet the given name Message: TypeError: object is not a function Stacktrace: TypeError: object is not a function at null.<anonymous> (./spec/greetSpec.js:8:12) 2) greet should greet no-one special if no name is given Message: TypeError: object is not a function Stacktrace: TypeError: object is not a function at null.<anonymous> (./spec/greetSpec.js:12:12) Finished in 0.015 seconds 2 tests, 2 assertions, 2 failures, 0 skipped
Jasmine tells us that it executed two test cases that contained a total of two assertions (or expectations), and because these expectations could not be satisfied, the test run ended with two failures.
It's time to satisfy the first expectation of our specification, in file src/greet.js:
'use strict'; var greet = function(name) { return 'Hello ' + name + '!'; }; module.exports = greet;
Another test case run reveals that we are getting closer:
.F Failures: 1) greet should greet no-one special if no name is given Message: Expected 'Hello undefined!' to equal 'Hello world!'. Stacktrace: Error: Expected 'Hello undefined!' to equal 'Hello world!'. at null.<anonymous> (spec/greetSpec.js:12:21) Finished in 0.015 seconds 2 tests, 2 assertions, 1 failure, 0 skipped
Our first test case passes – greet can now correctly greet people by name. We still need to handle the case where no name was given:
'use strict'; var greet = function(name) { if (name === undefined) { name = 'world'; } return 'Hello ' + name + '!'; }; module.exports = greet;
And that does the job:
.. Finished in 0.007 seconds 2 tests, 2 assertions, 0 failures, 0 skipped
We have now created a piece of software that behaves according to its specification.
You'll probably agree that our approach to create this unbelievably complex unit of software – the greet function – in a test-driven way doesn't prove the greatness of test-driven development in any way. That's not the goal of this chapter. It merely sets the stage for what's to come. We are going to create real, comprehensive software through the course of this book, and this is where the advantages of a test-driven approach can be experienced.