Send to Kindle

Thursday, May 27, 2010

Fab.js Testing

‹prev | My Chain | next›

Last night, I wrote my first legitimate fab.js test. It was a hard fought battle to figure out exactly how to do it, but ultimately I got it right. Tonight I am going to do it even better.

I refactor my test thusly:
exports.tests = ( function() {
var head = { url: { search : "?player=foo&x=1&y=2" } };

return [
function
bodyRespondsWithPlayer() {
var player;
function downstream (obj) { if (obj) player = obj.body; }

var upstream_listener = player_from_querystring.call(downstream);
upstream_listener(head);

this(player.id == "foo");
}
I think this is about as good as this test can be. Getting it here has given me a clearer understanding of what fab.js does.

Starting from the top, the downstream function behaves like a downstream (closer to the web client) fab.js app. It is meant to be called by the upstream app with a response object. For the purposes of my test, I expect that the response object will encapsulate the the player in the response body so I squirrel it away in a variable:
      var player;
function downstream (obj) { if (obj) player = obj.body; }
At this point things get interesting. The app that I am testing, player_from_querystring is a unary fab.js app. That means that it is the end point of the current request. Fab.js calls it a unary app based on the arity—it takes a single argument, the downstream app. This can be seen in the call to player_from_querystring:
      var upstream_listener = player_from_querystring.call(downstream);
What's interesting here is that I need to call() the player_from_querystring() function. I need to call() because this is how fab.js does it. Why it does it is part of what makes fabs.js so interesting.

The built-in javascript function call() evaluates the function being called (here, my player_from_querystring function/app). What is different about calling call() rather than invoking the function directly is what happens to the this variable inside the function. Normally, the this variable would refer to the function itself. But using call() sets the this keyword to the first argument of call. In this case, inside player_from_querystring the this variable will point to my downstream app.

Why does fab.js do that? Because upstream apps need to send responses back downstream. They cannot do so without a reference to the downstream app, to which they have convenient access to in the this keyword. Nice.

The fun on that line does not stop there, however. I assign the result of the call to a variable I name upstream_listener. Not all fab apps return a listener, but they do if they need to act on input from downstream. For my player_from_querystring app, I need to act on input from all the way downstream—I need to parse the query string supplied by the browser. So my player_from_querystring app will return a listener for that information and will expect to be called with a player query string (e.g. "?player=foo&x=1&y=2"). To simulate this in my test, I call the listener directly:
      var upstream_listener = player_from_querystring.call(downstream);
upstream_listener(head);
With that information, my player_from_querystring app replies to the downstream app with a player object. That gets stored in the player variable, which I can then test:
      this(player.id == "foo");
And yeah, I am testing fab.js, so the this variable in the test harness (copied directly from fab.js source) is part of the testing framework.

With that, I have a nice, small unit test and a much firmer grasp on what fab.js is doing under the covers.

For completeness, the fab.js app that passes this test:
function player_from_querystring() {
var out = this;
return function(head) {
if (head && head.url && head.url.search) {
var search = head.url.search.substring(1);
var q = require('querystring').parse(search);
var app = out({ body: {id: q.player, x: q.x || 0, y: q.y || 0} });
if ( app ) app();
}
else {
out();
}
};
}


Day #116

No comments:

Post a Comment