Saturday, June 15, 2013

Async Test Setup in Dart

‹prev | My Chain | next›

At this point, I think it's safe to say that I am huge Dart testing fan. I have hundreds of tests covering Dart for Hipsters (many of which are broken at the moment). I have another 100+ covering ICE Code Editor (which is being written to support 3D Game Programming for Kids, which in turn is blocking me from updating Dart for Hipsters). Bottom line: I love Dart tests.

I have gotten quite adept at writing them—regular, asynchronous, client-side, server-side. It's all good. But there is still lots that I do not know. Some stuff that I do not know is stuff I do not even know that I do not know—the stuff that will bite me when my current abstractions break down as I try new things. One thing that I know that I don't know is asynchronous test setup.

(I really enjoyed writing the above paragraph and I think it almost makes sense)

The unittest documentation for setUp(), concludes with:
The setupTest function can be asynchronous; in this case it must return a Future.
I do not really understand what it means for a setupTest to be asynchronous. I know from bitter experience that the tests within a group() sharing a setUp are asynchronous, but how can a setUp() itself be asynchronous and how does it help?

It so happens that many of the tests in the ICE Code Editor have a Future floating around. We use the editorReady future to indicate when all of the associated JavaScript has finished loading. To enable this to work in tests, we wrap the actual test in a function that will not be invoked until the expected asynchronous call from the future is made.

In one of the download tests, it looks something like:
    test("it downloads the source as a file", (){
      _test(_) {
        // actual test expectations go here
      }
      editor.editorReady.then(expectAsync1(_test));
    });
The expectAsync1 is built into Dart's unittest. It prevents the test from passing until it is called (with one argument). Instead of using it to test an asynchronous operation, I am using it to poll for the JavaScript libraries to finish loading before running an unrelated test. I do this a lot in ICE—in well over 50% of the tests I would guess.

But it now occurs to me that the editorReady is a Future. What if I move it up into the setUp() test to be returned?
    setUp((){
      editor = new Full(/* ... */);
      return editor.editorReady;
    });
Does that mean that I can get rid of my abuse of expectAsync? Indeed it does. I remove the expectAsyncs from both of the tests in the Download feature:
  group("Download", () {
    setUp((){
      editor = new Full(/* ... */);
      return editor.editorReady;
    });

    test("it downloads the source as a file", (){
      // Just test code now
    });

    test("closes the main menu", () {
      // Just test code now
    });
  });
And the tests still pass:
unittest-suite-wait-for-done
PASS: Download it downloads the source as a file
PASS: Download closes the main menu
All 2 tests passed.
unittest-suite-success 
If I comment out the return of the editor.editorReady, then both tests fail because pieces of the underlying editor are not ready (are null):
unittest-suite-wait-for-done

FAIL: Download it downloads the source as a file
  Caught The null object does not have a getter 'value'.
  
  NoSuchMethodError : method not found: 'value'
  Receiver: null
  Arguments: []
  ...
  
FAIL: Download closes the main menu
  Caught NoSuchMethodError : method not found: 'ace'
  Receiver: Instance of 'Proxy'
  Arguments: []
  ...
  
0 PASSED, 2 FAILED, 0 ERRORS
Exception: Exception: Some tests failed.
Holy cow. Without that expectAsync code, the intent of my tests are 100% clearer. I am amused by my own arrogance in thinking that I knew Dart testing. Clearly there is still much to learn. I find that a very exciting prospect.

But before finding any new stuff, I have roughly 50 tests that need to be cleansed of unnecessary expectAsync calls.


Day #783

No comments:

Post a Comment