Monday, July 15, 2013

The Stupidest Way to Stub HttpRequest Evar


As the title suggests, I am finally going to do it. I have been dancing around this for the better part of a week, but it is time. I am going to add stubbing to HttpRequest in Dart.

To date, I have been testing by hitting against an actual test server that needs to be up and running. For the most part this actually works and is especially stable under continuous integration. The tests are fairly readable as is the following unit test for reading records in Hipster MVC:
    group("HTTP get", (){
      test("it can parse responses", (){
        var model = new FakeModel();
        HipsterSync.
          call('get', model).
          then(
            expectAsync1((response) {
              expect(response, {'foo': 1});
            })
          );
      });
    });
In this case, it is not at all obvious what the source of that JSON response is (it is the default response from my test server). I would like to add the ability to stub HTTP requests in set up blocks. Something along the lines of:
    group("HTTP get", (){
      setUp((){
        HttpRequestX.respondWith("{'foo': 1}");
      });

      test("it can parse responses", (){ /* ... */});
    });
Since I have no hope of being able to inject new behavior into Dart's HttpRequest, my only choice is to have HttpRequestX.stub() inject my desired response into the actual server (I told you this was dumb).

So I add a static method to HttpRequestX:
class HttpRequestX {
  static Future respondWith(response) {
    return HttpRequest.request(
      'http://localhost:31337/stub',
      method: 'post',
      sendData: response
    );
  }
}
I am POSTing to the /stub resource of my test server, sending in the POST body the content that I want returned by the next request.

This means that I need for the server to honor POSTs to /stub:
var stub;
main() {
  HttpServer.bind('127.0.0.1', 31337).then((app) {
    app.listen((HttpRequest req) {
      if (req.uri.path.startsWith('/stub')) {
        addStub(req);
        return;
      }
      // handle other responses ...
    });
  });
}

addStub(req) {
  req.toList().then((list) {
    stub = new String.fromCharCodes(list[0]);

    HttpResponse res = req.response;
    res.statusCode = HttpStatus.NO_CONTENT;
    res.close();
  });
}
For now, I declare a global variable stub, which will hold the content of the currently stubbed response. If a request comes in for /stub, the addStub() function handles it. In there, I grab the body content with what I currently think is the cleanest way to read a request body in Dart. Then I respond back successfully.

Now, all that is left it to actually send the stubbed response. If defined, the stubbed response needs to go first, so the code handling stubs comes first in the app.listen() block:
var stub;
main() {
  HttpServer.bind('127.0.0.1', 31337).then((app) {

    app.listen((HttpRequest req) {
      if (stub != null) {
        req.response.write(stub);
        req.response.close();
        stub = null;
        return;
      }

      if (req.uri.path.startsWith('/stub')) {
        addStub(req);
        return;
      }
      // handle other responses ...
    });
  });
}
And that does the trick. With that, I can stub out a different response in my test, update my expectation:
    group("HTTP get", (){
      setUp((){
        return HttpRequestX.respondWith('{"foo": 42}');
      });

      test("it can parse responses", (){
        var model = new FakeModel();
        HipsterSync.
          call('get', model).
          then(
            expectAsync1((response) {
              expect(response, {'foo': 42});
            })
          );
      });
    });
And my test still passes:
CONSOLE MESSAGE: PASS: Hipster Sync HTTP get it can parse responses
Unfortunately, this does not seem to be a robust solution as I often see incorrect failures stemming from the default response coming back instead of the stub:
...
CONSOLE MESSAGE: PASS: Hipster Sync can parse empty responses
CONSOLE MESSAGE: FAIL: Hipster Sync HTTP get it can parse responses
  Expected: {'foo': 42}
    Actual: {'foo': 1}   Which: was <1> instead of <42> at location ['foo']
CONSOLE MESSAGE: PASS: Hipster Sync HTTP post it can POST new records
...
I am unsure why this might be occurring. If another test was causing an asynchronous request to be made, I would expect to see at least two tests fail. Instead it is just this one. Still, the future that is returned from HttpRequest.send() should not complete until the response is ready (meaning the stub is in place). Since setUp() returns this future, it should block until the stub is in place.

So my first experimentation with psuedo-stubbing in Dart is inconclusive. This feels pretty wrong to begin with and the false negatives are not encouraging. Still, this seems worth playing with a bit more. Tomorrow.


Day #813

No comments:

Post a Comment