Monday, October 7, 2013

Staircase to Dependency Hell


One of the many reasons that I love Dart is the testing. The built-in unittest is pretty darn amazing. And people are already building things like scheduled_test on top of it. I still haven't figured out when to use unittest and when to use scheduled test, but after last night, I may have found at least one rule of thumb.

While testing the Polymer.dart version of ICE Code Editor, I wound up creating a unittest test like:
      group("after JS has loaded and been evaluated", (){
        setUp(()=> Editor.jsReady);

        test("can still embed code", (){
          var later = createElement('ice-code-editor')
            ..xtag.src = 'embed_baz.html';

          document.body.append(later);

          Timer.run(
            expectAsync0((){
              expect(
                queryAll('ice-code-editor').last.shadowRoot.query('h1').text,
                contains('embed_baz.html')
              );
            })
          );
        });
      });
What bothers me about this is the crazy staircase of curly braces at the end. Admittedly, this is partly due to my stubbornness in insisting on one-concept-per-line-of-code. Still, staircases only appear in code when nested dependencies are present and this is no exception.

So let's see if scheduled_test can help. The scheduled_test add-on to unittest breaks asynchronous tasks into “schedules.” Hopefully that will be of particular use with the Timer.run(). I start by replacing unittest in the dev_dependencies section of my package's pubspec.yaml file:
name: ice_code_editor
version: 0.0.11
description: Code Editor + Preview
# ...
dev_dependencies:
  scheduled_test: any
Then pub install to grab the new package:
➜  ice-code-editor git:(polymer) ✗ pub install
Resolving dependencies.............................
Downloading http 0.7.5 from hosted...
Downloading scheduled_test 0.7.5 from hosted...
Dependencies installed!
The next step is to replace the unittest import with a scheduled_test import in my test file:
library ice_polymer_test;

import 'package:scheduled_test/scheduled_test.dart';
// Other imports here...

main() {
  // Tests here...
}
And, since scheduled_test is a drop-in replacement for unittest, all of my tests continue to pass:
PASS: [polymer] can embed code 
PASS: [polymer] can set line number 
PASS: [polymer] creates a shadow preview 
PASS: [polymer] creates an editor 
PASS: [polymer] multiple elements can embed code 
PASS: [polymer] multiple elements after JS has loaded and been evaluated can still embed code 
 
All 6 tests passed. 
Although the Timer.run() section of my test is bothering more than other parts of the test, I will start at the the top. The “after JS has loaded and been evaluated” group was written only to get the setUp() to block until the returned Editor.jsReady future completed. With scheduled_test, I no longer need that setUp() allowing me to move the Future into a schedule of the actual test:
      test("can still embed code", (){
        schedule(()=> Editor.jsReady);

        var later = createElement('ice-code-editor')
          ..xtag.src = 'embed_baz.html';

        document.body.append(later);

        Timer.run(
          expectAsync0((){
            expect(
              queryAll('ice-code-editor').last.shadowRoot.query('h1').text,
              contains('embed_baz.html')
            );
          })
        );
      });
Nice, I have already removed one step in my dependency staircase.

The next schedule in my test is adding my custom Polymer element to the DOM and then waiting one event loop for the ice-code-editor element to update the DOM. In the unittest version, the Timer.run() served the purpose of waiting one event loop. In the scheduled_test version, I can put that wait back where it belongs—with the code that adds the element to the DOM.

I cannot use a Timer.run() this time. Instead, I need to return a Future that will wait one event loop before completing. The easiest way that I know to do that is with the named Future.delayed constructor. A delay of zero should wait one event loop:
      test("can still embed code after JS is loaded and evaluated", (){
        schedule(()=> Editor.jsReady);

        schedule((){
          var later = createElement('ice-code-editor')
            ..xtag.src = 'embed_baz.html';

          document.body.append(later);

          return new Future.delayed(Duration.ZERO);
        });

        schedule((){
          expect(
            queryAll('ice-code-editor').last.shadowRoot.query('h1').text,
            contains('embed_baz.html')
          );
        });
      });
With that, I am back to a fully passing test suite:
PASS: [polymer] can embed code 
PASS: [polymer] can set line number 
PASS: [polymer] creates a shadow preview 
PASS: [polymer] creates an editor 
PASS: [polymer] multiple elements can embed code 
PASS: [polymer] multiple elements can still embed code after JS is loaded and evaluated 
 
All 6 tests passed.
More importantly, I have my staircase down to three steps. Given that the concepts in those steps are: test expectation, schedule, and test, I think I can live with that. Maintainability in tests is not quite as important as it is in actual code, but little wins like this can go a long way.


Day #897

No comments:

Post a Comment