Tuesday, December 25, 2012

Fake Mocks to Test HttpRequest Dart Apps

‹prev | My Chain | next›

In order to test some introductory code in Dart for Hipsters, I ended up introducing a MaybeMockHttpRequest class:
load_comics() {
  var list_el = document.query('#comics-list')
    , req = new MaybeMockHttpRequest();

  req.open('get', '/comics', true);

  req.on.load.add((res) {
    var list = JSON.parse(req.responseText);
    list_el.innerHtml = graphic_novels_template(list);
  });

  req.send();
}
I will use a bit of code generation trickery to strip the MaybeMock bit from the code that actually goes in the book. The bottom line is that I can use this mock HttpRequest to stand-in for a real HttpRequest object under test.

The MaybeMockHttpRequest class uses a static variable to determine whether all objects constructed by this class are the real thing or a mock version:
class MaybeMockHttpRequest {
  static var use_mock;

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

    return new HttpRequest();
  }
}
By default, the real thing is used. To force the faker, I call MaybeMockHttpRequest from my test:
  group("[main]", (){
    setUp(() {
      Main.MaybeMockHttpRequest.use_mock = true;
    });
    // ...
  });
The actual MockHttpRequest class defines only the methods used in my code:
class MockHttpRequest {
  open(_a, _b, _c) => true;
  get on => new NullObject();
  send() => true;
}
That NullObject beast is a simple object that returns instances of itself whenever any of its methods are invoked:
class NullObject {
  noSuchMethod(invocation) {
    return new NullObject();
  }
}
Ah, the magic of noSuchMethod(). Anyhow, this has very simple tests passing:
    test('the app returns OK', (){
      expect(Main.main, returnsNormally);
    });
I would like a more substantive test. In this case, I would like to verify that, on successful JSON response for the list of comics books in my collection, the web page is updated with the appropriate title.

To accomplish that, I need to either inject or hard-code a response. I think hard-coding might be an appropriate solution in this case, but, since injecting is more difficult, I opt for that approach tonight. To be clear, I am not seeking a general solution. I am most decidedly playing with code to learn. If the resulting test code seems robust enough, I may keep it—after all it is just test code—but I am not trying to build a library or anything close to reusable.

In the setUp for my test, I am already telling the MaybeMockHttpRequest class that it should mock requests. In addition to that, I now want to tell the MockHttpRequest class what the response should be:
    setUp((){
      Main.MaybeMockHttpRequest.use_mock = true;
      Main.MockHttpRequest.responseText = """
        [{"id":"42", "title": "Sandman", "author":"Neil Gaiman"}]
      """;

      document.body.append(el);
    });
I can support that fairly easily with a static variable and static setter:
class MockHttpRequest {
  static var _responseText;

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

  open(_a, _b, _c) => true;
  get on => new NullObject();
  send() => true;
}
The static variable/setter and instance getter is a bit of a hack, but it serves my purposes. It works even though the _responseText reference in the instance getter fails—happily Dart will look up the same variable at the class level, which is where it finds it defined.

This still does not work because that responseText is called via a callback:
load_comics() {
  var list_el = document.query('#comics-list')
    , req = new MaybeMockHttpRequest();

  req.open('get', '/comics', true);

  req.on.load.add((res) {
    var list = JSON.parse(req.responseText);
    list_el.innerHtml = graphic_novels_template(list);
  });

  req.send();
}
Dang, I don't think that awesome NullObject is going to work for the on property in MockHttpRequest. Instead, I create a MockEvents class with a load property:
class MockEvents {
  List load = [];
}
As pretty as the NullObject was, that one-line body is even better as it more closely mimics the real on property.

With that, I can update MockHttpRequest to call all of the callbacks in the load list when the fake HTTP request is sent:
class MockHttpRequest {
  static var _responseText;
  var on = new MockEvents();

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

  open(_a, _b, _c) => true;
  send() => on.load.forEach((cb){cb(null);});
}
With that, I make my test invoke the main() function and set my expectation that the page will soon contain the comic book title from my dummy JSON:
    test('populates the list', (){
      Main.main();
      expect(el.innerHtml, contains('Sandman'));
    });
And... it passes:


In the end, I somewhat like this approach. The three classes, MaybeMockHttpRequest, MockHttpRequest and MockEvents have 11 lines of code between them to replace the normal HttpRequest classes:
class MaybeMockHttpRequest {
  static var use_mock;

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

    return new HttpRequest();
  }
}

class MockHttpRequest {
  static var _responseText;
  var on = new MockEvents();

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

  open(_a, _b, _c) => true;
  send() => on.load.forEach((cb){cb(null);});
}

class MockEvents {
  List load = [];
}
I have a test that verifies that, when send() results in a call to the list of load event listeners, then the web page gets populated. To be sure, this is not the same as a full stack, integration test, but it is still useful. Best of all, the dart_analyzer barely bats an eye at this code. All that seems to bother it is the mixing of the static and instance method names for the responseText stuff:
file://your_first_dart_app/public/scripts/comics.dart:116:14: Field's getter and setter should be both static or not static
   115:
   116:   static set responseText(v) => _responseText = v;
                     ~~~~~~~~~~~~
Of course this bothers me a bit too, so I will likely fix it. But all in all, I am fairly happy with this.

That said, I wouldn't say "no" if Dart suddenly supported a mechanism to override responseText. Until that capability comes along, this will do.


Day #610

No comments:

Post a Comment