Thursday, April 17, 2014

Bare Minimum Polymer.dart Testing


I finally have my Polymer.dart tests passing again. But that is insufficient for Patterns in Polymer. I have to ensure that I am not programming by coincidence, which is a poor programming practice and disastrous book writing practice.

James Hurford was kind enough to give me a head start on this. He noted that he was able to get my Polymer.dart tests passing even without a few things that I had previously thought required—things like async() calls to flush the Polymer platform and onPolymer futures to ensure that platform was in place.

James hypothesized that the non-necessity of these actions might be due to the dev-channel Dart that he is running whereas I am still on stable 1.3. I have 1.4 at the ready, but begin with 1.3 to put that to the test.

I am explicitly invoking the Polymer.onReady future as part of my test, so I will just comment that out from my setUp() block:
@initMethod
main() {
  setUp((){
    // schedule(()=> Polymer.onReady);
    // More setup here...
  });
  // Tests here...
}
The schedule() here is not built into Dart's very excellent unittest library. Rather it comes from the also excellent scheduled_test package which extends unittest with some asynchronous goodness.

With that commented out, I also remove the async() calls. This was pulled into my Dart code because it was, at one point, necessary in both JavaScript and Dart versions of Polymer elements. I am not using this directly in the tests, but rather as a method on the Page Object that I am using to interact with the Polymer element:
class XPizzaComponent {
  String get currentPizzaStateDisplay => // ...
  Future addWholeTopping(String topping) {
    /* ... */
    return flush();
  }
  Future flush() {
    var _completer = new Completer();
    el.async((_)=> _completer.complete());
    return _completer.future;
  }
}
The async() method on Polymer elements is supremely handy when testing. It tells the Polymer element to immediately process all outstanding asynchronous operations like updating bound variables. It also tells the Polymer platform to “flush,” which updates all UI elements.

This came in extremely handy when the test interacted with the Polymer element (e.g. adding a whole topping to the <x-pizza> element) and needed to ensure that all updates had taken place before checking expectations. To put James' hypothesis to the test, I change the flush() method to return nothing instead of a Future:
class XPizzaComponent {
  String get currentPizzaStateDisplay => // ...
  Future addWholeTopping(String topping) {
    // ...
    return flush();
  }
   void flush() {}
}
And, just as James had found my tests all still pass:
➜  dart git:(master) ✗ ./test/run.sh
#READY
CONSOLE MESSAGE: unittest-suite-wait-for-done
CONSOLE MESSAGE: PASS: [defaults] it has no toppings
CONSOLE MESSAGE: PASS: [adding toppings] updates the pizza state accordingly
CONSOLE MESSAGE: 
CONSOLE MESSAGE: All 2 tests passed.
CONSOLE MESSAGE: unittest-suite-success
CONSOLE WARNING: line 213: PASS
So what gives? This works just fine in both Dart 1.3 and 1.4 without worrying about asynchronous operations. How did that happen?

Well, it turns out that there is a little nod to the asynchronous nature of Polymer that is still implicit. The scheduled test library is running each of those schedules in separate event loops. And, when each schedule finishes, it allows the main Dart event loop to continue processing whatever else it needs to—like Polymer bound variables and platform flushes. I can verify this by removing all schedules from my setUp():
  setUp((){
    // schedule(()=> Polymer.onReady);

    // schedule((){
      _el = createElement('<x-pizza></x-pizza>');
      document.body.append(_el);

      xPizza = new XPizzaComponent(_el);
      // return xPizza.flush();
    // });

    currentSchedule.onComplete.schedule(() => _el.remove());
  });
With that, all tests fail:
➜  dart git:(master) ✗ ./test/run.sh
#EOF
#READY
CONSOLE MESSAGE: unittest-suite-wait-for-done
CONSOLE MESSAGE: FAIL: [defaults] it has no toppings
  Caught ScheduleError:
  | Expected: '{"firstHalfToppings":[],"secondHalfToppings":[],"wholeToppings":[]}'
  |   Actual: ''
  |    Which: is different. Both strings start the same, but the given value is missing the following trailing characters: {"firstHal ...

CONSOLE MESSAGE: FAIL: [adding toppings] updates the pizza state accordingly
  Caught ScheduleError:
  | Expected: '{"firstHalfToppings":[],"secondHalfToppings":[],"wholeToppings":["green peppers"]}'
  |   Actual: '{"firstHalfToppings":[],"secondHalfToppings":[],"wholeToppings":[""]}'
  |    Which: is different.
  | Expected: ... ppings":["green pepp ...
  |   Actual: ... ppings":[""]}
  |                         ^
  |  Differ at offset 66
CONSOLE MESSAGE:
CONSOLE MESSAGE: 0 PASSED, 2 FAILED, 0 ERRORS
CONSOLE MESSAGE: Uncaught Error: Exception: Some tests failed.
I can make that pass by adding either the original async()/flush() or the schedule for Polymer.onReady, but I do not need both. Even so, I think that I will likely wind up recommending both in Patterns in Polymer. It better mirrors the approach in the JavaScript Polymer. Also, I think they make conceptual sense.


Day #37

2 comments:

  1. Thanks for finding this out. I knew there was something fishy, but didn't think to try it without schedule(() {})

    ReplyDelete
  2. I did a quick test again, and it seems you do still need the async() and/or Polymer.onReady, but only if you've a sophisticated element like the markdown-markup element I created. Then I think that's a special case, where the markup is extracted from between the content tags, parsed, then the result is inserted back into the element. This takes a second render of the element, so we have to wait for it to finish that. Things like that will occur again and again I think. There are probably other combinations which need the delay caused by async() and Polymer.onReady, so I think you're right in still recommending this approach.

    ReplyDelete