Tuesday, July 6, 2010

First Attempt at Binary Fab.js Testing with Vows.js

‹prev | My Chain | next›

Up today: using vows.js to test binary fab.js apps. Over the past week, I have gotten the hang of testing unary (fab) apps.

There should not be too much of a difference between the two. One of the nice things about (fab) apps is their stackable nature. Binary (middleware) apps can be stacked on top of each other—each call the next in line upstream. This nature plays nicely with testing. A binary app + unary (upstream) app behaves the same as a unary by itself, which I already know how to test with vows.js.

Yesterday, I factored out my API call of the unary app into:
var api = {
fab: {
send_obj: function(obj) {
return function () {
var topic = this;
var upstream_listener = player_from_querystring.call(
function(obj) { topic.callback(null, obj && obj.body); }
);
upstream_listener(obj);
};
}
}
};
The topic local variable is being assigned to the vows.js topic that is currently being tested. I need to do that because I call topic.callback to actually send data to my tests.

I am testing the init_comet (fab) app today, so I will need to supply it with a faux upstream app that quacks like the real-life upstream app—player_from_querystring. The real-life player_from_querystring either returns a Javascript player object or undefined.

I need to adapt yesterday's unary API caller with a binary + unary (fab) app that can vary the unary / upstream response. This might serve:
var api = {
fab: {
when_upstream_replies_with: function(obj) {
return function () {
var topic = this;

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

var upstream_listener = unary.call(
function(obj) { topic.callback(null, obj && obj.body); }
);
upstream_listener(obj);
};
}
}
};
I am not sure about the longish name for the API call: when_upstream_replies_with. I think it captures what I am trying to do here well enough and hopefully it will read. I just wish it were shorter.

At any rate, when the upstream (fab) app replies with the supplied obj, I create a dummy upstream app that replies with the object (recall that fab.js apps have the this variable set to the downstream app). I can then obtain my testable, unary app by supplying the upstream dummy app as the argument to the binary app being tested:
        var upstream = function() { this(obj); },
unary = init_comet (upstream);
After that, I am back to unary app testing. I call the combined unary app the same way that fab.js would. I supply an anonymous function to be assigned to the this variable in the fab app so that responses can be captured. Finally I call the vows.js topic.callback to set the topic.

With that, I ought to be able to write my test. First up, I expect that the unary should send a cookie based on the player's uniq ID:
var suite = vows.describe('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");
}
}
}).export(module);
Nice. That is a very succinct very readable test for a fairly complex interaction between apps.

Unfortunately, when I run this I get:
cstrom@whitefall:~/repos/my_fab_game$ vows --spec

♢ init_comet

lib/init_comet.js:8
"Set-Cookie": "MYFABID=" + obj.body.uniq_id } })
^
TypeError: undefined is not a function
at CALL_NON_FUNCTION (native)
at listener (lib/init_comet.js:8:73)
at Function.<anonymous> (/home/cstrom/repos/my_fab_game/test/init_comet.js:14:37)
at Function.<anonymous> (lib/init_comet.js:5:16)
at Object.<anonymous> (/home/cstrom/repos/my_fab_game/test/init_comet.js:17:39)
at run (/home/cstrom/.node_libraries/.npm/vows/0.4.5/package/lib/vows/suite.js:125:31)
at EventEmitter. (/home/cstrom/.node_libraries/.npm/vows/0.4.5/package/lib/vows/suite.js:195:40)
at EventEmitter.emit (events:42:20)
at /home/cstrom/.node_libraries/.npm/vows/0.4.5/package/lib/vows/suite.js:147:58
at EventEmitter._tickCallback (node.js:48:25)
NOTE TO SELF: it is a best practice to name the test file differently from the app file to better tell them apart in the stack trace.

The error itself is coming from my library. It took me a bit to noodle through that one. My binary actually sends data several times back downstream by calling the return value:
      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"})

// ...
This blows up on me because the API call that I am making does not send in a function that returns a function to be called. That is easily remedied:
var api = {
fab: {
when_upstream_replies_with: function(obj) {
return function () {
var topic = this;

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

var upstream_listener = unary.call(
function(obj) {
topic.callback(null, obj);
return function listener() {return listener;};
}
upstream_listener(obj);
);
};
}
}
};
That is not the end of my woes, however. Now, when I run my tests, I get:
cstrom@whitefall:~/repos/my_fab_game$ vows --spec

♢ init_comet

/home/cstrom/repos/my_fab_game/test/init_comet.js:23
upstream_listener(obj);
^
TypeError: undefined is not a function
at CALL_NON_FUNCTION (native)
at Object.<anonymous> (/home/cstrom/repos/my_fab_game/test/init_comet.js:23:9)
at run (/home/cstrom/.node_libraries/.npm/vows/0.4.5/package/lib/vows/suite.js:125:31)
at EventEmitter.<anonymous> (/home/cstrom/.node_libraries/.npm/vows/0.4.5/package/lib/vows/suite.js:195:40)
at EventEmitter.emit (events:42:20)
at /home/cstrom/.node_libraries/.npm/vows/0.4.5/package/lib/vows/suite.js:147:58
at EventEmitter._tickCallback (node.js:48:25)
at node.js:204:9
Ah. When I was testing my unary app yesterday, the preconditions involved sending in data from downstream (the browser). The upstream_listener was the vehicle for sending that data. Now, not only to I not care about that, but also there is no upstream listener returned from init_comet.

So I remove it from my API call:
var api = {
fab: {
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;};
}
);

};
}
}
};
With that, I finally have a passing test:
cstrom@whitefall:~/repos/my_fab_game$ vows --spec

♢ init_comet

with a player
✓ sets a session cookie


♢ player_from_querystring

with a query string
✓ is player
✓ has unique ID
✓ has X coordinate
✓ has Y coordinate
without explicit X-Y coordinates
✓ has X coordinate
✓ has Y coordinate
POSTing data
✓ is null response

✓ OK » 8 honored (0.122s)
That was not quite as easy as I had hoped, but it is a start. I will continue with my binary (fab) app testing tomorrow.


Day #156

No comments:

Post a Comment