Thursday, March 19, 2015

Simple Solutions to Asynchronous Polymer.dart Testing


In the development release of Dart and Polymer.dart, the simplest that I can write is still complex:
  group("<hello-you>", (){
    setUp((){ /* Add element here... */ });

    test("has a shadowRoot", (){
      Polymer.onReady.then(expectAsync((_) {
        expect(
          query('hello-you').shadowRoot,
          isNotNull
        );
      }));
    });
  });
I add my custom element to the page and expect that, by virtue of Polymer goodness, it has a Shadow DOM. This is not a completely useless test—verifying that Polymer is decorating an otherwise unknown HTML element tells me a lot. Still, it would be nice if there was not so much… asynchronous goop obfuscating an otherwise simple expectation.

At its heart, this test is just:
        expect(
          query('hello-you').shadowRoot,
          isNotNull
        );
But it is hard to see with everything else that needs to take place to ensure that the test runs and that it runs in content_shell, the dirt-simple headless testing solution for Dart.

My last thought yesterday remains my first thought tonight. Why not simplify this with a Polymer-specific test matcher? This turns out to be the wrong idea, but bear with me...

I start with a function that tries to wrap the expected asynchronous call like so:
polymerExpect(actual, matcher) {
  Polymer.onReady.then(expectAsync((_) {
    expect(actual, matcher);
  }));
}
The main reason that this is wrong is that the actual value would have already been determined before this helper method is invoked. That is, if I try to rewrite my test with this helper, it would look like:
    test("has a shadowRoot", (){
      polymerExpect(
        query('hello-you').shadowRoot,
        isNotNull
      );
    });
Since there is no wait-for-Polymer or any other delay in here, this code is evaluated immediately. I query for the <hello-you> element in the test page and send its shadowRoot into polymerExpect() without waiting for Polymer to be ready. In other words, I send null as the "actual" value—not what I want.

While contemplating how to rewrite this so that evaluation of the shadowRoot can be deferred, I finally realize how I should have done this from the start—and it does not even require using the scheduled_test asynchronous wrapper around unittest.

If the setup() block in unittest returns a Future, the test runner will block until that Future completes. So all I have to do is return Polymer.onReady from setup():
  group("<hello-you>", (){
    setUp((){
      // Add the element here...
      return Polymer.onReady;
    });

    test("has a shadowRoot", (){
      expect(
        query('hello-you').shadowRoot,
        isNotNull
      );
    });
  });
With that, my test is as simple (and readable) as possible and it runs in Dartium and content_shell alike.

I feel pretty dumb for not recognizing that before. As penance for my stupidity, I write two new tests for this element. It is from an example very early in Patterns in Polymer, so it does not have ID attributes to make it easier to find elements within the shadow DOM. That aside, the tests are fairly straight forward:
    test("name updates the template", (){
      _el.your_name = "Bob";
      _el.async(expectAsync((_){
        var h2 = _el.shadowRoot.querySelector('h2');
        expect(h2.text, 'Hello Bob');
      }));
    });

    test("color changes", (){
      var h2 = _el.shadowRoot.querySelector('h2');
      expect(h2.style.cssText, '');

      _el.shadowRoot.querySelector('input[type=submit]').click();
      _el.async(expectAsync((_){
        expect(h2.style.cssText, startsWith('color'));
      }));
    });
The <hello-you> element has the your_name property bound to the <h2> of its template. The first test verifies that the <h2> contains “Bob” if the your_name property is set to "Bob". The second verifies that the <h2> starts life without a color style, but is randomly assigned a color when the button in <hello-you> is clicked.

Both tests rely on my old friend async(), which mostly ensures that the Polymer UI and computed properties have been updated. With those tests added, I have three tests passing:
PASS
1       PASS
Expectation: <hello-you> has a shadowRoot .

2       PASS
Expectation: <hello-you> name updates the template .

3       PASS
Expectation: <hello-you> color changes .

All 3 tests passed
The added tests are nice (and verify that multiple tests work), but the real win is removing the unnecessary asynchronous code from my tests. Many Polymer tests involve some form of asynchronous code. Best not to start with things cluttered.


Day #3

No comments:

Post a Comment