Thursday, February 23, 2012

Async Testing in Dart

‹prev | My Chain | next›

Today, I continue exploring unit testing in Dart. I am using the bleeding edge in-browser testing suite, which has worked fairly well so far. It seems as though it is meant to be run right on top of the live application, which could be interesting—add a <script> tag and see the test suite results overlayed on the application.

I have tested most of the Collection aspects of the HipsterCollection class from my Hipster MVC framework. Tonight I hope to cover the Ajax features. First up, the fetch() method. I am not sure how to go about doing this, so I start by verifying that calling fetch() directly on the base class fails (a sub-class needs to define url()):
  test('HipsterCollection fetch() fails without a url', () {
    HipsterCollection it = new HipsterCollection();
    Expect.throws(() {it.fetch();});
  });
That succeeds, and by success, I have verified that an exception is being thrown:


Next, I define a dummy implementation of the HipsterCollection base class:
class TestHipsterCollection extends HipsterCollection {
  String get url() => 'test.json';
}
And try to write a spec that at least will not crash:
  test('HipsterCollection fetch() fetches url', () {
    HipsterCollection it = new TestHipsterCollection();
    it.fetch();
  });
This fails. And by "fails" I mean that I now have a failing test:


In the Dart console, I see a CORS exception:
XMLHttpRequest cannot load file:///home/cstrom/repos/dart-comics/tests/test.json. Cross origin requests are only supported for HTTP.
  Result: ERROR Caught Error: NETWORK_ERR: XMLHttpRequest Exception 101
That test.json file exists. Even if I load it in the test page via script tag, I still get a CORS violation.

I deal with with the same thing when I use Jasmine to test JS applications, but at least with those I can use libraries like sinon.js to fake XHR responses. I cannot do that in Dart without somehow telling my MVC library to use a different XHR object. Luckily, I can do just that.

Based on Backbone.sync, the HipsterSync class allows me to redefine the manner in which storage operations are performed. By default, it uses XHR, but I can swap that out for a no-op:
  test('HipsterCollection fetch() fetches url', () {
    HipsterSync.sync = (method, model, [options]) {
      print("[sync] $method / $model");
    };

    HipsterCollection it = new TestHipsterCollection();
    it.fetch();
  });
When I run the test suite now, I see the no-op print() output in the Dart console:
[sync] get / Instance of 'TestHipsterCollection'
And all of my tests are again passing:


They might be passing, but I am not really testing anything other than that the application does not crash. To be sure, there is value in that, but I can do better. The HipsterSync call is an asynchronous callback. Rooting through the bleeding edge test suite, I come across the asyncTest() function. This is not a vows.js-style assertion that a specific callback is invoked. Rather, it provides a mechanism to say that any number of callbacks will be invoked and a way to signify that they were actually called.

In this case, I expect that my sync callback will be invoked once. To verify that it has been called, I invoke callbackDone() inside the sync method:
asyncTest('HipsterCollection fetch() fetches via callback', 1, () {
    HipsterSync.sync = (method, model, [options]) {
      print("[sync] $method / $model");
      callbackDone();
    };

    HipsterCollection it = new TestHipsterCollection();
    it.fetch();
  });
And it works!


If I had specified an expectation of more callbacks than were actually invoked:
asyncTest('HipsterCollection fetch() fetches via callback', 42, () { /* ... */ });
, then the spec no longer passes:


Actually, that is not so much failing as preventing the suite from completing. That is better than nothing, but it would be preferable to say that, if the proper number of callbacks have not been invoked in a certain amount of time, then the test fails. Something to investigate another day.


Day #305

No comments:

Post a Comment