Tuesday, December 11, 2012

Custom Test Matchers in Dart

‹prev | My Chain | next›

Yesterday, I was pleased to find that Dart tests originally written with the pre-bleeding-edge (so sharp you'll get cut just looking at it edge?) still worked with the recent version of the unittest library. Mercifully, this will cut down on the amount that I need to rewrite in the upcoming revised edition of Dart for Hipsters. Still, some things must change, so tonight I have a look at some of them.

As I noted yesterday, the asyncTest testing method is deprecated. Since I rely on it exclusively to test the fetch() method in my sample, I need to replace it with the Dart Way of doing it. The simplest old test that I have verifies that the callback is reached:
  group('HipsterCollection fetch() callback', () {
    callbackSync(method, model) {
      callbackDone();
      // ...
    }

    asyncTest('is invoked', 1, () {
      HipsterSync.sync = callbackSync;
      HipsterCollection it = new TestHipsterCollection();
      it.fetch();
    });
  });
The idea was that the built-into-unittest callbackDone() method needs to be invoked in order for the test to pass. By setting the data synchronization method to my dummy callbackSync method, I can verify that the MVC collection is using the synchronization layer.

The new way of asynchronous testing in Dart is with the expectAsyncN methods, where “N” describes the number of parameters expected in the async call. In the case of my synchronization layer, I expect 2 parameters to be supplied: the method (create, read, delete, etc) and the object being manipulated. So I need to use expectAsync2:
  group('HipsterCollection fetch() callback', () {
    solo_test('is invoked', () {
      HipsterSync.sync = expectAsync2((method, model) {
        var completer = new Completer();
        completer.complete([]);
        return completer.future;
      });

      HipsterCollection it = new TestHipsterCollection();
      it.fetch();
    });
  });
I use solo_test so that I can just focus on this one test. I will change that back to just test when I am done.

With that, I have my test passing again:


And now, I am using non-deprecated testing methods.

That is a fairly simple test. The next test is a bit more involved. Not only does it complete a Dart Future, but there is also an event generated. The old test, with callbackDone() call still in it reads:
    solo_test('dispatches a load event', () {
      HipsterCollection it = new TestHipsterCollection();

      it.
        on.
        load.
        add((event) {
          callbackDone();
        });

      it.fetch();
    });
I replace it with an expectAsync1() (it expects a single event):
    solo_test('dispatches a load event', () {
      HipsterSync.sync = protectAsync2((method, model) {
        return new Future.immediate([]);
      });
      HipsterCollection it = new TestHipsterCollection();

      it.
        on.
        load.
        add(expectAsync1((event) {
          // ...
        }));

      it.fetch();
    });
The test is passing at this point, but it is not actually testing anything. I can make it pass with a bit more meaning by setting an expectation inside the expectAsync1() call. A reasonable check is that the collection property of the event is present:
    solo_test('dispatches a load event', () {
      HipsterSync.sync = protectAsync2((method, model) {
        return new Future.immediate([]);
      });
      HipsterCollection it = new TestHipsterCollection();

      it.
        on.
        load.
        add(expectAsync1((event) {
          expect(event.collection, isNotNull);
        }));

      it.fetch();
    });
That is a pretty solid test right there. Under normal circumstances, I would probably leave it at that, but I am curious as to how to test that I have a particular class. In this case, how can I ensure that the event has an instance of HipsterCollection?

The easiest way is to set the expectation against the return of an is:
      it.
        on.
        load.
        add(expectAsync1((event) {
            expect(event.collection is HipsterCollection, equals(true));
        }));

      it.fetch();
That works, but is not ideal since a failure would generate a non-obvious error along the lines of:
Expected: <true> but: was <false>.
This is where the TypeMatcher comes in handy. I can define a subclass of the TypeMatcher to match HipsterCollections:
class HipsterCollectionMatcher extends TypeMatcher {
  const HipsterCollectionMatcher() : super("HipsterCollection");
  bool matches(item, MatchState matchState) => item is HipsterCollection;
}
By virtue of the super() redirect, I get nicer failure messages along the lines of:
Expected: HipsterCollection but: was <Instance of 'HispterModel'>.
That could come in pretty darn handy in efforts to maintain a large codebase and test suite.

After fixing a few more tests in the same manner, I have my small test suite again in the green, but now there are no deprecated test methods involved:


I call it a night here. Up tomorrow: websockets.



Day #596

No comments:

Post a Comment