Testing REST API with LearnBoost’s Tobi + Vows.js
I've been looking for a clean, framework-independent way of doing white-box API testing using Node.js. For a long while, the things that popped up when doing a quick scan of the Node Package Manager package lists weren't ticking all of the right boxes: zombie.js is going for a full browser simulation but doesn't provide a simple Browser.post() method (you have to use selectors to find the form.submit button and then fire a click() on it), Node's native http.Client is too low-level and doesn't do cookies, and various other http request wrappers weren't quite cutting it either. I think I've found a solution for this particular version of the problem: LearnBoost's Tobi combined with Vows.js is letting me do clean REST API testing, with a minimum of hassle, and all the built-in sugar-coated goodness of should.js fluent assertions. For example:
var HOST = 'localhost';
var PORT = 80;
var API = "/api/user/create";
var tobi = require('tobi'),
vows = require('vows'),
assert = require('assert');
var newbie = function() { return tobi.createBrowser(PORT, HOST) };
// -------------------------------------------------------------
// Macros.
// -------------------------------------------------------------
var macroPostOnlyApiChecks = function(url) {
return {
'GET': {
topic: function() {
var browser = newbie();
browser.get(url, this.callback);
},
'should fail.': function(res, $) {
res.should.not.have.status(200);
}
},
'Empty POST': {
topic: function() {
var browser = newbie();
browser.post(url, this.callback);
},
'should fail.': function(res, $) {
res.should.not.have.status(200);
}
}
};
};
var macroCreateUserOk = function(suite) {
return {
'Create user': {
topic: function() {
var browser = newbie();
var data = { signupUsername: '', };
data = JSON.stringify(data);
browser.post('/api/user/create', data, this.callback);
},
'should succeed.': function(res, $) {
res.should.have.status(200);
console.log(res);
// Pass created user credentials back to
// the calling suite for later use.
if (suite) {
}
}
}
};
};
// -------------------------------------------------------------
// User API Test Suite
//
// Run me with: vows api.user.create.vows.js --spec
// -------------------------------------------------------------
var suite = vows.describe('User API Test Suite');
// Batches are executed sequentially.
// Contexts are executed in parallel.
suite.addBatch(macroPostOnlyApiChecks(API));
suite.addBatch(macroCreateUserOk());
suite.export(module);
Future steps then include using the macros to help set up other tests that require a valid user, etc. Overall, this is the most straightforward solution I've yet found for the problem of testing a REST API while also faking a session.
Javascript Decorators
One reason why Javascript rocks:
As a use case, imagine you want to restrict a certain set of functions to only run if you are logged in. Doing stuff like this is ridiculously easy with first-class functions.
Here's a generic decoration example:
(function()
{
var original = function(paramOne, paramTwo) {
console.log('original: ' + paramOne + ' ' + paramTwo);
};
var decorator = function(originalFn) {
return function(paramOne, paramTwo) {
console.log('decorator: ' + paramOne + ' ' + paramTwo);
originalFn(paramOne, paramTwo);
};
};
var fnTable = {};
var registerCallback = function(url, callback) {
fnTable[url] = callback;
};
registerCallback('/abc', original);
registerCallback('/def', decorator(original));
fnTable['/abc']('one', 'two');
fnTable['/def']('three', 'four');
})();
Update: Here's the specific way you'd do this with node.js/Express:
Since all of the URL handlers you register have the same signature, it's easy to add precondition checks to the handlers via decorators.
var decorator = function(originalFn) {
return function(req, res) {
if (req.session.userLoggedIn) {
originalFn(req, res);
} else {
redirectSomewhere(res);
}
}
}
app.get('/url', decorator(original));
app.post('/other', decorator(original));
You get preconditions essentially for free, which is a damn sight better than adding the if() block to each and every handler function. Also, if you need more preconditions in the future, you can just stack them.
Geocoding with Mapstraction v2 and Google Maps v3
There isn't a whole lot of info out there about how to use Mapstraction, especially the v2 API, so here's one thing I've had to pick up along the way.
I spent a little time hacking together a geocoder module that uses the Google Maps v3 API, but was having to track down why it wasn't loading properly.