Wednesday, July 7, 2010

Take 2: Testing Binary Fab.js Apps with Vows.js

‹prev | My Chain | next›

I encountered some difficulties yesterday trying to use vows.js to test a binary fab.js app. Hopefully I can do a bit better today.

At the end of yesterday's work, I came to the conclusion that my difficulties stemmed from a mistaken belief that binary and unary (fab) apps were inherently the same—at least from a testing perspective. That is not as silly as it sounds. A binary (fab) app + a unary app == a unary app. My mistake was in not recognizing that the preconditions were different.

The unary app on which I cut my vows.js teeth responded to input from downstream apps (e.g. the browser). The binary (middleware) app that I was trying to test yesterday responded to input from upstream apps. I eventually corrected my mistake and ended up with a decent vows.js test for my comet initialization (fab) app:
'init_comet').
addBatch({
'with a player': {
topic: api.fab.when_upstream_replies_with({body: {id:1, uniq_id:42}}),
'sets a session cookie': function(obj) {
assert.equal(obj.headers["Set-Cookie"], "MYFABID=42");
}
}
})
I rather like that. When the upstream app replies with a player object, this binary app should set a cookie in response.

That is all well and good, but my challenge today surrounds how fab.js supports sending multiple chunks of replies. It does so via downstream listeners that return themselves to listen for more responses. An empty response signals to downstream (or the browser) that the connection can be closed. Since I am testing a comet connection, I do not want to close the connection.

The structure of the init_comet (fab) app being tested looks like:
function init_comet (app) {
return function () {
var out = this;

return app.call( function listener(obj) {
if (obj && obj.body) {
out({ headers: { "Content-type": "text/html",
"Set-Cookie": "MYFABID=" + obj.body.uniq_id } })

({body: "<html><body>\n" })

({body: "<script type=\"text/javascript\">\"123456789 123456789 123456789 123456789 123456789 12345\";</script>\n"})
// ... More of these dummy scripts to force chrome to recognize the comet stream
(obj);
}
return listener;
});
};
}
The chain of function out returning a function that gets called and returns another function that in turn gets called, was fairly easy to support in vows.js—I did it the same way that fab.js does it:
    when_upstream_replies_with: function(obj) {
return function () {
var topic = this;

var upstream = function() { this(obj); },
unary = init_comet(upstream);

unary.call(
function(obj) {
topic.callback(null, obj);
return function listener() {return listener;};
}
);
};
}
The challenge that I have left for myself today is how do I test that the second response from my middleware is the opening of an HTML document? As written, my API call will only set the topic of the test to the first response—the topic.callback. All other chunks are lost to the listener function that does nothing but return itself.

In the end, I decide that the topic of my from-downstream-tests needs to collate all of the chunks:
var api = {
fab: {
when_upstream_replies_with: function(upstream_obj) {
return function () {
var topic = this;

var upstream = function() { this(upstream_obj); },
unary = init_comet(upstream);

var chunks = [];
unary.call(
function listener(obj) {
chunks.push(obj);
if (obj == upstream_obj || typeof(obj) == "undefined") {
topic.callback(null, chunks);
}
return listener;
}
);
};
}
}
};
That's a bit of a hack because one of the things that I will be testing is that my chunks will include the upstream (player) object and my API call is deciding that the topic is ready when the current chunk is the same as the supplied fake player.

I am not happy with that, but I have no other good way of solving this. I cannot use the absence of a call to my downstream listener as the signal that the topic ready. Well, I could try setting a timeout, but that seems even more hackish than what I already have. I will give this a go and change as needed.

Happily, that makes my actual test rather nice:
      'sets a session cookie': function(chunks) {
assert.equal(chunks[0].headers["Set-Cookie"], "MYFABID=42");
},

'sends the opening HTML doc': function(chunks) {
assert.match(chunks[1].body, /<html/i);
},

'sends the player': function(chunks) {
assert.deepEqual(chunks[chunks.length-1], {body: {id:1, uniq_id:42}});
}
True, I do have to have a bit more knowledge of the order of my chunked replies, but the order is something set in my target (fab) app, so that knowledge seems OK to me. I can even use the array of chunks to test that I can poke Chrome into recognizing data in the comet stream:
      'send 1000+ bytes to get Chrome\'s attention': function(chunks) {
var byte_count = 0;
for (var i=0; i < chunks.length; i++) {
var chunk = chunks[i];
if (chunk && chunk.body && typeof(chunk.body) == "string") {
byte_count = byte_count + chunk.body.length;
}
}
assert.isTrue(byte_count > 1000);
},
That could be cleaned up a bit if I were using a Javascript library like underscore.js, but I will leave that for another day. A few more tests and I have my init_comet (fab) app completely covered:
cstrom@whitefall:~/repos/my_fab_game$ vows --spec

♢ init_comet

with a player
✓ sets a session cookie
✓ sends the opening HTML doc
✓ send 1000+ bytes to get Chrome's attention
✓ sends the player
without a player
✓ terminates the downstream connection
with an invalid player object
✓ terminates the downstream connection
I may look to try adding underscore.js to my testing toolkit tomorrow. That or I will give testing another binary (fab) app a try.

Day #157

No comments:

Post a Comment