Tiny Candy Hammers

Quickstart Tutorial on Testing in Node.js

I recently started using Node.js for a project and decided to write tests to speed up my development. After looking at a few of the testing modules I decided on Mocha and SuperTest. SuperTest makes it very easy to test HTTP endpoints and Mocha makes writing tests in general very easy. This tutorial will demonstrate the basics of testing a web app using these modules.

Assumptions

I am assuming that you already have basic javascript experience or experience with a language with similar syntax. You should also already have Node.js and npm (Node Package Manager) installed. If you don’t have Node.js and npm installed you can still learn from this tutorial but you won’t be able to run any of the examples.

Setup

If you want to follow along, you will need to create a directory to contain the project. I am calling the directory ‘nodejs_testing_example’. The first file we will create is ‘package.json’:


{
  "name": "nodejs_testing_example",
  "version": "0.0.1",
  "description": "A simple testing example using supertest and mocha",
  "author": "William Beene <bwr@tinycandyhammers.com>",
  "devDependencies": {
    "supertest": "0.1.x",
    "mocha": "1.3.x"
  }
}

Node.js projects typically have a package.json to describe the project and its dependencies. Npm can use our package.json to automatically download the development dependencies we listed. We can do this by running the following command from the project directory.


npm install -d

Now you should have a directory called ‘node_modules’ inside of the project directory that contains the modules Mocha and SuperTest.

Test outline

Now that we have our project setup and dependencies downloaded, let’s create an outline of our test. Creating the outline first will hopefully give a clearer picture of the structure of a test in Mocha.

The following file needs to be placed in a directory called ‘test’ underneath our project directory. The name of the file isn’t especially important but I like to give it the same name as the module I am testing.


// This is test/server.js
var request = require('supertest');
var assert = require('assert');

describe('Server', function() {
	describe('GET /get-data', function() {
		it('responds with default data');
	});

	describe('POST /set-data', function() {
		it('responds with success');
	});

	describe('GET /get-data', function() {
		it('responds with new data');
	});
});

Now we can run our test with the following command from the project directory:


./node_modules/.bin/mocha --reporter spec

You should see following output:

Pending tests are tests that haven’t been implemented yet.

(Optional) Running tests with make

You may have noticed I used `make test` to run the tests instead of the command I previously gave you. If you have make installed and want to use it to run your tests you can create the following Makefile and place it in your project directory.


# This is Makefile

test:
	./node_modules/.bin/mocha --reporter spec

.PHONY: test

Server implementation

We create a basic router that handles the routes ‘/set-data’ and ‘/get-data’. Any other request is replied to with a 404.

The route ‘/set-data’ accepts a JSON post body and uses it to set the server’s global state ‘serverData’. I’ve included a very basic function for parsing the JSON post body.

The route ‘/get-data’ simply returns a JSON body with the value of ‘serverData’.


// This is server.js
var http = require('http');
var url = require('url');

var CONTENT_TYPE_JSON = {'Content-Type': 'application/json'};

var serverData = "default data";

// Router handles all requests and routes based on request path
var router = function(req, res) {
	var reqUrl = url.parse(req.url, true);
	switch(reqUrl.pathname) {
		case '/set-data':
			// Set serverData from postData
			parseJSONPost(req, res, function(postData, res) {
				res.writeHead(200, CONTENT_TYPE_JSON);
				var output = { result: 'success' };
				res.end(JSON.stringify(output));
			});
			break;
		case '/get-data':
			// Send serverData to the client
			res.writeHead(200, CONTENT_TYPE_JSON);
			var output = { data: serverData };
			res.end(JSON.stringify(output));
			break;
		default:
			// Send 404
			res.writeHead(404, CONTENT_TYPE_JSON);
			var output = { result: '404' };
			res.end(JSON.stringify(output));
			break;
	}
};

// parseJSONPost will read POST body and parse as JSON object
// which is then passed to the onResult callback
function parseJSONPost(req, res, onResult) {
	var postBody = "";

	req.on('data', function(chunk) {
		postBody += chunk;
	});

	req.on('end', function() {
		var postData = JSON.parse(postBody);
		onResult(postData, res);
	});
}

// Create the server using our router and export the server
var server = http.createServer(router);
module.exports = server;

Test implementation

Now that the server is implemented, let’s go back to the test and fill in the code for the actual tests. With SuperTest we can make http requests just like a browser would and then check the response data very easily using asserts.


// This is test/server.js
var request = require('supertest');
var assert = require('assert');

// Here we load our server.js as a module
var server = require('../server');

describe('Server', function() {
	describe('GET /get-data', function() {
		it('responds with default data', function(done) {
			request(server)
				.get('/get-data')
				.end(function(err, res) {
					// Make sure there was no error
					assert.equal(err, null);

					var body = res.body;
					assert.equal(body.data, 'default data');

					// Finish asynchronous test
					done();
				});
		});
	});

	describe('POST /set-data', function() {
		it('responds with success', function(done) {
			request(server)
				.post('/set-data')
				.send({data: 'new data'})
				.end(function(err, res) {
					assert.equal(err, null);
					var body = res.body;
					assert.equal(body.result, 'success');
					done();
				});
		});
	});

	describe('GET /get-data', function() {
		it('responds with new data', function(done) {
			request(server)
				.get('/get-data')
				.end(function(err, res) {
					assert.equal(err, null);
					var body = res.body;
					assert.equal(body.data, 'new data');
					done();
				});
		});
	});
});

Ok, let’s run the tests again:

Looks like we made a mistake somewhere. If we read the test error we will see that our server returned ‘default data’ from the route ‘/get-data’ instead of ‘new data’.

Fixing the server

If we look back at the server implementation, it is obvious that we forgot to actually update the value of ‘serverData’ when an HTTP POST is made to ‘/set-data’. We can fix this by adding one line:


// This is an excerpt from server.js

// Router handles all requests and routes based on request path
var router = function(req, res) {
	var reqUrl = url.parse(req.url, true);
	switch(reqUrl.pathname) {
		case '/set-data':
			// Set serverData from postData
			parseJSONPost(req, res, function(postData, res) {

				// NEW LINE IS HERE
				serverData = postData.data;

				res.writeHead(200, CONTENT_TYPE_JSON);
				var output = { result: 'success' };
				res.end(JSON.stringify(output));
			});
			break;
		case '/get-data':
			// Send serverData to the client
			res.writeHead(200, CONTENT_TYPE_JSON);
			var output = { data: serverData };
			res.end(JSON.stringify(output));
			break;
		default:
			// Send 404
			res.writeHead(404, CONTENT_TYPE_JSON);
			var output = { result: '404' };
			res.end(JSON.stringify(output));
			break;
	}
};

And now we will run the tests again:

Everything works! If this was a real project you could now commit your changes to your version control system of choice and start working on some new features.

Isn’t there something missing?

Yea, something is missing. Currently, there is no way to actually run our server. We need to add one more file to start the server.


// This is main.js
var server = require('./server');

server.listen(8080, '127.0.0.1');

console.log('Server started on port 8080');

And now you can run the server using:


node main.js

And if we go to http://localhost:8080/get-data in a browser you will see the following:

Conclusion

I hope this tutorial will help you with your own projects. Personally, I am pleased with the ease of testing using Mocha and SuperTest.

I’ve posted the code on github at https://github.com/bwr/nodejs_testing_example

This is the first tutorial I have written so besides comments on the content I would appreciate comments on the execution and structure of the tutorial itself.

Further Reading

Mocha: http://visionmedia.github.com/mocha/
Supertest: https://github.com/visionmedia/supertest#readme

Versions used

I hate when I find an old tutorial and nothing works because the tutorial hasn’t been updated to work with the latest versions so I am listing the versions I am using here for future readers.

node: v0.8.3
npm: 1.1.43
mocha: 1.3.2
supertest: 0.1.2

blog comments powered by Disqus