Saturday, May 29, 2010

The Right Way to Test Fab.js Binary Apps

‹prev | My Chain | next›

Bleh.

I had a harder than expected time testing my fab.js binary app last night. I ultimately got my test passing and testing what it is supposed to test. But it wasn't pretty.

Recall that I am trying to test a binary (middleware) fab app that sits between an upstream app that returns a player object and downstream (the web client). It stores player information and initiates a comet connection back to the web client:
function init_comet (app) {
return function () {
var out = this;

return app.call( function listener(obj) {
if (obj && obj.body) {
var downstream = out({ headers: { "content-type": "text/html" },
body: "<html><body>\n" })

({body: "<script type=\"text/javascript\">\"123456789 123456789 123456789 123456789 123456789 12345\";</script>\n"})
({body: "<script type=\"text/javascript\">\"123456789 123456789 123456789 123456789 123456789 12345\";</script>\n"})
// ... more of these body-scripts to push past Chrome's
// buffer
}
return listener;
});
};
}
My first test was meant to test that the first chunk returned downstream contained "<html><body>\n". The test that I ended with last night:
  var upstream = function() { this({body:{id:42}}); },
downstream = init_comet (upstream);

function
bodyRespondsWithStartHtml() {
var out = this,
already_tested = false;

downstream.call(function listener(obj) {
if (!already_tested) out(obj.body === "<html><body>\n");
already_tested = true;
return listener;
});
}
There are a couple of problems there. First, I should not name the return value of init_comet as downstream. I am in the downstream context, but the return value of a binary fab app is a unary app. So I rename it unary:
  var upstream = function() { this({body:{id:42}}); },
unary = init_comet (upstream);

function
bodyRespondsWithStartHtml() {
var out = this,
already_tested = false;

unary.call(function listener(obj) {
if (!already_tested) out(obj.body === "<html><body>\n");
already_tested = true;
return listener;
});
}
The next problem is that already_tested binary. I knew that it was crazy to use that last night, but, for the life of me, I could not figure out how to do without it. I probably could have used a pomodoro break or something because it does not take long for me to figure out the right way to do it tonight:
  var upstream = function() { this({body:{id:42}}); },
unary = init_comet (upstream);

return [
function
bodyRespondsWithStartHtml() {
var out = this;

unary.call(function (obj) {
out(obj.body === "<html><body>\n");
return function listener() {return listener;};
});
}
That is much better. When I call the unary app returned from my init_comet fab app, I set the this variable inside the unary to that anonymous function (this is what javascript's call() function does). This is just normal fab.js stuff—it leverages call like crazy to make app chaining work (which is how jQuery does it as well). The unary app then replies back downstream via this anonymous function, at which point I can test it.

The last line is a bit trickier (and what I could not figure out last night):
        return function listener() {return listener;};
What is the point of returning a function that does nothing other than returning itself? The answer is that I am not sending a single chunk back downstream. After sending the initial HTML document, I send several more chunks:
out({ headers: { "content-type": "text/html" },
body: "<html><body>\n" })
({body: "<script type=\"text/javascript\">\"123456789 123456789 123456789 123456789 123456789 12345\";</script>\n"})
({body: "<script type=\"text/javascript\">\"123456789 123456789 123456789 123456789 123456789 12345\";</script>\n"})
// ... more of these body-scripts to push past Chrome's
// buffer
The only reason I do this is to make sure that Chrome starts responding to comet responses. It is not really necessary behavior—it only handles yet another browser idiosyncrasy. Still, it is worth knowing how to test. Sending multiple chunks like that is perfectly normal fab.js stuff. After sending the first chunk downstream, the return value is called with more HTML, the return value of that is again called with more HTML, and so on. If I wanted to tell downstream that I was done with it, I would have called it with no arguments. As it is, the last call in that stack is more output:
         // ... more of these body-scripts to push past Chrome's
// buffer
({body: "<script type=\"text/javascript\">\"123456789 123456789 123456789 123456789 123456789 12345\";</script>\n"})
({body: "<script type=\"text/javascript\">\"123456789 123456789 123456789 123456789 123456789 12345\";</script>\n"})
This signals to downstream to expect more output, which is how I get the fab.js backend to communicate player movement later (aka comet calls). At any rate, it is this call stack that necessitates the function that returns itself in my test:
        return function listener() {return listener;};
Each return value is called by the init_comet app. For this unit test, I do not care about anything else in that stack, which is why my test ignores it.

Armed with that knowledge, I can now do all sorts of testing of binary fab.js apps. For instance, I can test that the second chunk is Chrome comet padding:
    function
bodyPadsCometSoChromeWontIgnoreMe() {
var out = this;
unary.call(function(obj) {
return function second_listener(obj) {
out(/123456789/.test(obj.body));
return function listener() {return listener;};
};
});
}
I ignore the response to the anonymous function that I supply to the unary. I do my test in the second listener, then ignore the remaining responses. With my test harness, I now have two passing tests:
cstrom@whitefall:~/repos/my_fab_game$ node test.js
Running 2 tests...
bodyRespondsWithStartHtml: true
bodyPadsCometSoChromeWontIgnoreMe: true
Done. 2 passed, 0 failed.
I am really getting the hang of this stuff. I have some refactoring on tap for tomorrow, then I would like to play with Cucumber's newly found integration on the v8 javascript engine.

Day #118

No comments:

Post a Comment