Tuesday, July 13, 2010

BDD Fab.js with Vows.js

‹prev | My Chain | next›

Yesterday, I taught my comet initialization fab.js middleware to ignore upstream connection closes (closing a connection is not of much use when doing comet). Today, I need to teach the upstream app, player_from_querystring, to close connections properly.

I have the functionality of building a player object from a query string working quite well. Not coincidentally, it is well tested with vows.js. The remaining code, dealing with invalid query strings and telling downstream that we are done talking looks like:
function player_from_querystring() {
var out = this;
return function(head) {
if (head && head.url && head.url.search) {
// Build a (q)uerystring object and generate a uniq_id
var app = out({ body: {id: q.player, x: q.x || 0, y: q.y || 0, uniq_id: uniq_id} });
if ( app ) app();
}
else {
out();
}
};
}
Before doing anything else, I am eliminating the distinction between the out and app local variables. The out local variable is, by convention, the downstream (close to the browser) listener in fab.js. When I call it with an object representation of the player, I send that object back downstream (middleware decorates it in a browser-friendly way). The downstream listener returns another listener, which I assign to the local variable app, in case the current app has any more to say. Here, there is nothing more to say, so I call app() with empty arguments.

The thing is, out and app refer to the same downstream listener. The last thing the out listener does is return a reference to itself. I suppose another listener could be returned, but in my experience it is the always the same listener. Even if it were not the same listener, from the current app's perspective, it is still the downstream listener, so why not re-use the out variable which, by fab.js convention, refers to the downstream:
      out = out({ body: {id: q.player, x: q.x || 0, y: q.y || 0, uniq_id: uniq_id} });
if (out) out();
Much better, now my brain does not begin to wonder what app is. There is only out and I know what that is.

Now to actually drive the new functionality. My vows.js test currently report:
cstrom@whitefall:~/repos/my_fab_game$ vows --spec test/player_from_querystring_test.js

♢ 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 » 7 honored (0.103s)
That last test is now wrong. Instead of expecting a null response when data is POSTed to this resource, I should expect a 4xx HTTP error. Additionally, I should expect to that an invalid querystring (i.e. without required fields) should also result in a 4xx HTTP error. For now, I will mark the invalid querystring topic as pending and will fix the "POSTing data" topic / test:
    'POSTing data': {
topic: api.fab.response_to({body: "foo"}),

'should raise an error': function (obj) {
assert.equal(obj.status, 406);
}
},
'invalid querystring': "pending"
// topic: api.fab.response_to({
// url: { search : "?foo=bar" },
// headers: { cookie: null }
// }),
With that, I get my expected failure:
cstrom@whitefall:~/repos/my_fab_game$ vows --spec test/player_from_querystring_test.js 

♢ player_from_querystring

- invalid 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
✗ should raise an error
TypeError: Cannot read property 'status' of undefined
at Object.<anonymous> (/home/cstrom/repos/my_fab_game/test/player_from_querystring_test.js:73:25)
at runTest (/home/cstrom/.node_libraries/.npm/vows/0.4.5/package/lib/vows.js:99:26)
at EventEmitter.<anonymous> (/home/cstrom/.node_libraries/.npm/vows/0.4.5/package/lib/vows.js:72:9)
at EventEmitter.emit (events:42:20)
at /home/cstrom/.node_libraries/.npm/vows/0.4.5/package/lib/vows/context.js:24:44
at EventEmitter._tickCallback (node.js:48:25)
at node.js:204:9

✗ Errored » 6 honored ∙ 1 errored ∙ 1 pending (0.117s)
Aahhh! I am in a new testing framework and a relatively new application framework, but my development cycle fits like an old glove. Change the message or make it pass. Here, I can move right onto the make it pass state with:
function player_from_querystring() {
var out = this;
return function(head) {
if (head && head.url && head.url.search) {
// Build a (q)uerystring object and generate a uniq_id
var out = out({ body: {id: q.player, x: q.x || 0, y: q.y || 0, uniq_id: uniq_id} });
if (out) out();
}
else {
out({ status: 406 });
}
};
}
And yup, that passes:
cstrom@whitefall:~/repos/my_fab_game$ vows --spec test/player_from_querystring_test.js 

♢ player_from_querystring

- invalid 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
✓ should raise an error

✓ OK » 7 honored ∙ 1 pending (0.106s)
Now I move onto driving the behavior when invalid query strings are supplied. The test topic sends an invalid query string in the header. The expectation for that topic is that the status returned will be 406 (Invalid Format Request):
    'invalid querystring': {
topic: api.fab.response_to({
url: { search : "?foo=bar" },
headers: { cookie: null }
}),
'should raise an error': function (obj) {
assert.equal(obj.status, 406);
}
}
Running this test, it currently fails (as expected):
cstrom@whitefall:~/repos/my_fab_game$ vows --spec test/player_from_querystring_test.js 

♢ 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
✓ should raise an error
invalid querystring
✗ should raise an error
» expected 406,
got undefined (==) // player_from_querystring_test.js:82

✗ Broken » 7 honored ∙ 1 broken (0.121s)
I make it pass by adding a conditional ensuring that a player attribute is present:
function player_from_querystring() {
var out = this;
return function(head) {
if (head && head.url && head.url.search) {
// Build a (q)uerystring object and generate a uniq_id
if (q.player) {
out = out({ body: {id: q.player, x: q.x || 0, y: q.y || 0, uniq_id: uniq_id} });
out();
}
else {
out = out({ status: 406 });
}

}
else {
out({ status: 406 });
}
};
}
With that, I have both of my new tests passing:
cstrom@whitefall:~/repos/my_fab_game$ vows --spec test/player_from_querystring_test.js 

♢ 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
✓ should raise an error
invalid querystring
✓ should raise an error


✓ OK » 8 honored (0.100s)
Before calling it a day, I smoke test both of these new options along with a valid player via curl:
# Fails with POSTed data
cstrom@whitefall:~/repos/my_fab_game$ curl -i http://localhost:4011/comet_view -d foo=bar
HTTP/1.1 406 Not Acceptable
Connection: keep-alive
Transfer-Encoding: chunked

# Fails with an invalid querystring
cstrom@whitefall:~/repos/my_fab_game$ curl -i http://localhost:4011/comet_view?foo=bar
HTTP/1.1 406 Not Acceptable
Connection: keep-alive
Transfer-Encoding: chunked

# Success!
cstrom@whitefall:~/repos/my_fab_game$ curl http://localhost:4011/comet_view?player=foo\&x=250\&y=350
<html><body>
<script type="text/javascript">window.parent.player_list.new_player({"id":"foo","x":250,"y":350,"uniq_id":"322f2ded412c1f5161876535ce441688"})</script>
Perfect!

That is a good stopping place for today. Up tomorrow: perhaps some refactoring or moving on to other aspects of my (fab) game.


Day #163

No comments:

Post a Comment