Tuesday, August 23, 2011

Batching Vows (in express-spdy)

‹prev | My Chain | next›

Over the past few nights, I think that vows.js has more or less beaten me into submission. I had been trying to do some setup outside of vows' batches, but bitter experience seems to be telling me that this is a bad idea.

Ultimately, what I want to do inside my test suite is establish a simple express-spdy server, open a SSL/SPDY connection, send a SPDY request and test the response. Rather than do any of that outside of the tests themselves, I can do them in "batches" to be run sequentially. That way, I can be guaranteed that connections are ready when I expect them to be.

The server needs to be in the first "batch" of tests—it needs to be running in order for connections to be made:
vows.describe('Express SPDY response').
addBatch({
  '[setup]': {
    'establish a simple SSL express server': function() { // ... }
  }
})
I can then craft the SPDY request and open a client SSL connection in the next batch:
.addBatch({
  '[setup]': {
    'open an SSL connection to the server': function() { // ... },
    'craft a simple SPDY request': function() { // ... }
  }
})
In neither of these batches do I have a topic or an actual vow (as seen below, the "vows" just setup a server, a SSL client connection, and create a request object). I am abusing vows batches to ensure that things are done in the proper order. I apply a convention of describing these setup batches with the '[setup]' label. I think it kinda-sorta almost works.

In the last batch, I send the created request over the established connection and perform the actual test:
.addBatch({
  'Sending the request': {
    topic: function () {
      // Send request here
    },
    'should get a response with no Keep-Alive': function(cframe) {
      // Assertions here
    }
  }
})
I put the sending of the request in a separate batch because it needs to have both the connection and request in place. Multiple contexts in the same batch can be run in any order (or in parallel). In practice, they are executed sequentially, but I would rather not set myself up for failure in the future.

I could have also made the actual sending of the packet a sub-context of the connection setup. The implementation would have been cleaner, but I stick with separate batches to keep the --spec output as pretty (and readable) as possible.

As mentioned, the actual implementation of the test is muddled—by use of global variables that pass the connection and SPDY request between batches. Ick, I know, but this is test code. Maintainability, while important, is not as important as readability. So...
var connection,
spdy_request;

vows.describe('Express SPDY response').
...
The complete test looks like:
var connection,
    spdy_request;

vows.describe('Express SPDY response').
addBatch({
  '[setup]': {
    'establish a simple SSL express server': function() {
      var server = express.createServer(options);

      server.get('/', function(req, res){
        res.send('wahoo');
      });

      server.listen(PORT);
      return true;
    }
  }
}).
addBatch({
  '[setup]': {
    'open an SSL connection to the server': function() {
      connection = tls.connect(PORT, 'localhost', options, function() {});
    },
    'craft a simple SPDY request': function() {
      spdy_request = spdy.createControlFrame(
        spdy.createZLib(),
        { type: spdy.enums.SYN_STREAM, streamID: 1, flags: 0 },
        { version: 'HTTP/1.1', url: '/', method: 'GET' }
      );
    }
  }
}).
addBatch({
  'Sending the request': {
    topic: function () {
      var callback = this.callback;

      var parser = spdy.createParser(spdy.createZLib());
      connection.pipe(parser);

      parser.on('cframe', function(cframe) {
        if (cframe.headers.type == spdy.enums.SYN_REPLY) {
          callback(null, cframe);
        }
      });

      connection.write(spdy_request, function(){});
    },
    'should get a response with no Keep-Alive': function(cframe) {
      assert.notEqual(cframe, undefined);
      assert.notEqual(cframe.data, undefined);
      assert.notEqual(cframe.data.nameValues, undefined);
      assert.equal(cframe.data.nameValues['connection'], undefined);
    }
  }
})
And the output from the test is:
➜  express-spdy git:(master) ✗ vows test/response-test.js --spec

♢ Express SPDY response

  [setup]
    ✓ establish a simple SSL express server
  [setup]
    ✓ open an SSL connection to the server
    ✓ craft a simple SPDY request
  Sending the request
    ✗ should get a response with no Keep-Alive
      » expected undefined,
        got      'keep-alive' (==) // response-test.js:74

✗ Broken » 3 honored ∙ 1 broken (0.043s)
I rather like that. I have two, separate [setup] blocks for the server and the connection. Finally, I have my test that actually tries to send the request and validate that there is not a keep-alive issued on the connection.

At this point, I am good to go. I have my necessary failing test describing the undesired behavior. Just as importantly, I have nice, readable spec output.

So how do I fix that failure? Well, first I check through express-spdy, but it is kinda tiny:
➜  express-spdy git:(master) ✗ wc -l *js
  25 express.js
   1 index.js
  67 spdy.js
  93 total
There is nothing in there except for hooking this prototype up to that method and choosing this server implementation if that argument is present. It is really, really small.

So where do I need to make the change then? Ugh. If it is in express.js, things are going to get ugly fast. I will have to pull in a lot more than 93 lines of javascript to get that working. So, mostly in an effort to avoid the mere thought of that work, I start in node-spdy. Specifically, I look through response.js in node-spdy, when what do my wondering eyes see?
var Response = exports.Response = function(cframe, c) {
// ...
  this._headers = {
    'Connection': 'keep-alive'
  };
//...
};
Aw, fer cryin' out loud. I spent the last four nights flailing through self-inflicted bad vows.js code only to find that I am testing the wrong thing?

Yup, that's exactly what I did. Commenting out the 'Connection' line in node-spdy response.js leaves me with a passing test:
➜  express-spdy git:(master) ✗ vows test/response-test.js --spec

♢ Express SPDY response

  [setup]
    ✓ establish a simple SSL express server
  [setup]
    ✓ open an SSL connection to the server
    ✓ craft a simple SPDY request
  Sending the request
    ✓ should get a response with no Keep-Alive

✓ OK » 4 honored (0.031s)
Ah well, it is not a complete waste. I have a much better grasp on vows.js topics, contexts, sub-contexts and vows. I also have a very pretty spec for express-spdy (even if it is testing underlying node-spdy functionality). Up tomorrow I will switch back to node-spdy and pursue the issue there.


Day #122

No comments:

Post a Comment