Monday, January 14, 2013

Testing Futures in Dart

‹prev | My Chain | next›

I keep thinking that I am almost done with my latest Dart research chain. And this time, I think that I am, but before I move back to finishing off Gaming JavaScript, I need to figure out how to test futures and isolates in Dart. I have already explored asynchronous testing in Dart quite a bit, but I need to make sure that I have futures working properly.

In Dart for Hipsters, I introduce completers and futures with a simple code example:
main() {
  var completer = new Completer();

  var future = completer.future;
  future.then((message) {
    print("Future completed with message: $message");
  });
  completer.complete("foo");
}
Since I immediately complete the completer, the end result of running this code is to simply print out:
Future completed with message: foo
The question is how do I test this. The print() method complicates things, but I use my usual trick of overriding print() for my testing purposes. In this case, I forward the message onto a supplied callback:
main() {
  // ...
}

var cb;
print(msg) => cb(msg);
To enable this to be testable in a separate file, I add a library declaration at the top of the file:
library simple_completer;

main() {
  // ...
}

var cb;
print(msg) => cb(msg);
Now, in the test file, I import the unittest library and my simple_completer.dart library so that I can run my tests:
library completer_test;

import 'package:unittest/unittest.dart';

import '../simple_completer.dart' as Simple;

run() {
  group("[simple completers]", (){
    test('can complete in the future', (){
      // TESTS GO HERE
    });
  });
}
But how to test? The Dart matcher libary includes a completer() matcher that I would love to use in order to match my expectation. Unfortunately, the completer is self-contained within the library's main() method. I could manually create a new matcher and complete in the print() statement, but this seems like cheating. Instead, I fallback to a simple async test:
    test('can complete in the future', (){
      Simple.cb = expectAsync1((msg){
        expect(msg, equals("Future completed with message: foo"));
      });
      Simple.main();
    });
I set the callback variable in the Simple libary to a function that accepts a single parameter (that's what the 1 in expectAsync1() indicates). And inside that function, I set the expectation of the print message.

And amazingly, it works:
➜  isolates git:(master) ✗ dart test/test.dart
unittest-suite-wait-for-done
PASS: [simple completers] can complete in the future

All 1 tests passed.
unittest-suite-success
I am so surprised that this works with my first try that I have to intentionally break it to ensure that I really am testing what I think I am testing. So I change the test to expect a "bar" in the printed message instead of a "foo" and find:
➜  isolates git:(master) ✗ dart test/test.dart
unittest-suite-wait-for-done
FAIL: [simple completers] can complete in the future
  Expected: 'Future completed with message: bar'
       but: was 'Future completed with message: foo'.
So it seems that I really do have a test in place. Asynchronous testing in Dart is crazy easy.

Just because I enjoy playing with Dart testing matcher so much, I must see how the completion matcher works. The answer? Pretty much as neatly as do the other testing matchers. I set the expectation on the future as to which message that I expect to receive in the future. Then, when the associated completer finishes at some future point, the test executes:
    test('can complete once', (){
      var completer = new Completer();
      var future = completer.future;

      expect(future, completion("foo"));

      completer.complete("foo");
    });
I am saying that I expect that my future to be a completion with the value "foo", which it is:
➜  isolates git:(master) ✗ dart test/test.dart
unittest-suite-wait-for-done
PASS: [simple completers] can complete in the future
PASS: [simple completers] can complete once

All 2 tests passed.
Very nice. I will pick back up tomorrow with isolates testing, then I really need to get back to Gaming JavaScript.


Day #630

No comments:

Post a Comment