I was looking for the tools for weeks to write easy to run & write tests for node. Finally we’ve settled with Vows + Should + Tobi.
Vows is an asynchronous testing framework for Node.JS. Should is an extension on top of Node’s assert module. While Tobi is a really nice and easy to use request-faking library for Node.
How Mocha failed?
A very serious contender to Vows was Mocha, but it failed at a ridiculous stage, we could not make it wait for the database connection event. We have tried many things, but they simply did not work. The problem was that due to the async nature, our script’s `beforeEach` call was handled before the database connection was set up. On the other hand, if we’ve tried to put all or part of our tests into an event handler, then the tests were simply skipped as `make test` could not find any calls to `describe` and its friends before it finished running.
Why not node-request?
Actually, Tobi had a contender, node-request. We’ve found both libraries relatively easy to use, but Tobi seemed to be more developer friendly. It has a really nice API, it extends should.js.
How vows overcame our problem?
With vows we could easily overcome our problem. The only important thing was to really understand what batches and context are meant to mean, and what parallel and sequential processing they involve.
As you should know, batches contain context, that contain tests to be run. Batches are run sequentially, one after the other. This way we can isolate the database environment. On the other hand, inside a batch contexts are run in parallel, thus you should not rely on anything coming from outside of your function under test.
An example for batches and contexts
Our first test was about the user registration. Here is its code
var tobi = require('tobi'),
vows = require('vows'),
mainapp = require('../server'),
server = mainapp.app,
should = require('should'),
Db = require('backbone-mongodb/lib/db'),
_connection = null,
browser = tobi.createBrowser(server);
var db_callback = function(callback) {
mainapp.db.on('database', function(status){
var error = status == 'open' ? null : status;
if (error) callback(new Error('Could not connect to database'));
else {
_connection = Db.getConnection();
callback(error, _connection);
}
})
};
vows.describe('Registration')
.addBatch({
'database': {
topic: function() { db_callback(this.callback); },
'is available': function(err, db) {
should.not.exist(err);
should.exist(db);
}
}
})
.addBatch({
'clear database': {
topic: function() {
var callback = this.callback;
mainapp.statusapp.Users._withCollection(function(err, collection){
if(err) callback(err);
else collection.remove({}, function(err, result) {
if(err) callback(err);
else collection.count(callback);
});
});
},
'worked': function(err, length) {
should.not.exist(err);
length.should.equal(0);
}
}
})
.addBatch({
'is working': {
topic: function() {
browser.post(
'/register',
{
login: 'mylogin',
password: 'password',
password2: 'password'
},
this.callback
);
},
'without errors': function(res, $){
console.log('ok')
res.should.have.status(303);
mainapp.statusapp.Users.length.should.equal(1);
}
}
})
.addBatch({
'if username exists': {
topic: function() {
var callback = this.callback;
request.post({
uri: baseurl + '/register',
json: {
login: 'mylogin',
password: 'password',
password2: 'password'
}},
function(err, res, body) {
callback(err, [res, body]);
});
},
'should fail': function(err, data) {
mainapp.statusapp.Users.length.should.equal(1);
should.exist(err);
}
},
"if passwords don't match": {
topic: function() {
var callback = this.callback;
request.post({
uri: baseurl + '/register',
json: {
login: 'newuser',
password: 'password',
password2: 'otherpass'
}},
function(err, res, body) {
callback(err, [res, body]);
});
},
'should fail': function(err, data) {
should.not.exist(err);
var res = data[0],
body = data[1];
mainapp.statusapp.Users.length.should.equal(1);
res.should.have.status(200);
}
}
})
.export(module);
As you can see, we first wrote the code to connect to the database. This was really easy with vows, unlike with Mocha. Then we went on to set up the very basic test environment, and we’ve deleted all our users from the database. (Yeah, you should never run these tests on a production server .:)) After these we’ve started to write the real tests.
In the first batch we are creating a user, thus later batches check that the user should exist. A nice way of seeing what batches and contexts mean is when the first batch passes, but one of the later con