Thursday, July 11, 2013

Mocking HttpRequest the Dumb Way in Dart


To stub out HttpRequest in some smaller Dart projects, I have been using a MaybeMockHttpRequest class:
class MaybeMockHttpRequest {
  static var use_mock;

  factory MaybeMockHttpRequest() {
    if (MaybeMockHttpRequest.use_mock) return new MockHttpRequest();

    return new HttpRequest();
  }
}
Under normal circumstances, creating a new MaybeMockHttpRequest() will actually produce an instance of HttpRequest. But, in my test setup, I can set the use_mock class variable:
    setUp((){
      Main.MaybeMockHttpRequest.use_mock = true;
      // ...
    });
With that, the MaybeMockHttpRequest constructor will produce a MockHttprequest object that quacks like an HttpRequest, but does not actually allow network connections.

This is a dumb approach for no fewer that two reasons. First, it is pretty awful to have MaybeMockHttpRequest scattered throughout my code:
  void fetch() {
    var req = new MaybeMockHttpRequest();
    req.onLoad.listen((event) {
      var list = JSON.parse(req.responseText);
      _handleOnLoad(list);
    });
    req.open('get', url, async: true);
    req.send();
  }
This obscures the intent of my code solely to allow testing. The whole point of testing code is to make it more robust, accurate and maintainable. But if the only way to do it is to make the code less readable, then I am at best breaking even.

The other reason that this is dumb is that I am not even writing this in such a way that the compiler can take advantage of tree shaking. The compiler has no way to know if use_mock will never change. Given that, dart2js has no choice but to compile that dynamic logic so that it will be invoked every time that my code creates a new instance of MaybeMockHttpRequest.

To add insult to injury, even dartanlyzer complains about this code now:
[warning] The return type 'MockHttpRequest' is not a 'MaybeMockHttpRequest', as defined by the method '' (/home/chris/repos/dart-comics/public/scripts/comics.dart, line 405, col 47)
[warning] The return type 'HttpRequest' is not a 'MaybeMockHttpRequest', as defined by the method '' (/home/chris/repos/dart-comics/public/scripts/comics.dart, line 407, col 12)
It is this warning on which I am going to start tonight, because as bad as my other two issues are, this reflects a gap in my understanding of Dart.

My understanding had always been that factory constructors in Dart could create object instances of things other than the containing class. For instance, a pretty string factory might simply generate a string prefixed with the word “Pretty:"
class PrettyName {
  factory PrettyName(name) {
    return "Pretty $name";
  }
}
But that no longer seems to be the case as far as dartanalyzer is concerned. It also seems the dart language spec disagrees with me:
The return type of a factory whose signature is of the form factory M or the form factory M.id is M if M is not a generic type
Bother. Something else that needs changing in Dart for Hipsters.

I suppose that I can understand the change. It seems a little odd to create an instance of another class from the current class. Still my maybe-mock class seemed an ideal use-case for doing just that. Given that it is no longer kosher, I need to switch to a static method instead of a constructor:
class MaybeMockHttpRequest {
  static var use_mock;

  static httpRequest() {
    if (MaybeMockHttpRequest.use_mock) return new MockHttpRequest();

    return new HttpRequest();
  }
}
With that, my really dumb mocking scheme becomes:
  void fetch() {
    var req = MaybeMockHttpRequest.httpRequest();

    req.on.load.add((event) {
      var list = JSON.parse(req.responseText);
      _handleOnLoad(list);
    });
    req.open('get', url, async: true);
    req.send();
  }
As for the MockHttpRequest class itself, I still use a duck-typed class rather than something that extends the real thing:
class MockHttpRequest {
  static var _responseText;

  StreamController _loadStream;
  Stream onLoad;

  MockHttpRequest() {
    _loadStream = new StreamController.broadcast();
    onLoad = _loadStream.stream;
  }

  static stubResponseTextWith(v) => _responseText = v;
  get responseText => _responseText;

  open(_a, _b, {async, user, password}) {}
  send() => _loadStream.add(this);
}
The stream controller allows me to build my own onLoad stream to which I can add events in response to request open() calls:
    setUp((){
      Main.MaybeMockHttpRequest.use_mock = true;
      Main.MockHttpRequest.stubResponseTextWith("""
        [{"id":"42", "title": "Sandman", "author":"Neil Gaiman"}]
      """);
      // ...
    });

    test('populates the list', (){
      Main.main();
      Timer.run(
        expectAsync0(() {
          expect(el.innerHtml, contains('Sandman'));
        })
      );
    });
And that works. But it remains unsatisfactory (even if dartanalyzer is appeased). Hopefully I can come up with something better in the next couple of days.


Day #809

No comments:

Post a Comment