Tuesday, November 25, 2014

Better Polymer.dart Tests with Futures


I lamented Polymer.dart's JavaScript-y callback nature yesterday. But I didn't do much about it. Ultimately I cannot do much until the library matures into more of a Dart library. The Patterns in Polymer book needs to track the library, not my weakly held, strong opinions (and I do admit that it makes sense to track the JavaScript Polymer for now).

That said, I do think it worth exploring Dart futures as a means for cleaning up my tests.

Last night's test looks like:
    group('syncing <input> values', (){
      var input;

      setUp((){
        input = _container.append(createElement('<input>'));
        syncInputFromAttr(input, _el, 'state');
        _el.model.firstHalfToppings.add('pepperoni');
      });

      test('updates the input', (){
        _el.async(expectAsync((_){
          expect(
            input.value,
            startsWith('First Half: [pepperoni]')
          );
        }));
      });
    });
For a small test, that is not too horrible. Problems would arise as the complexity of the Polymer element grows.

The main problem with the above test is in the actual test:
      test('updates the input', (){
        _el.async(expectAsync((_){
          expect(
            input.value,
            startsWith('First Half: [pepperoni]')
          );
        }));
      });
The expectAsync() call in there is a nod to the asynchronous nature of Polymer and my attempt to account for it in my test. I need to wait for Polymer to invoke the supplied callback, which is what wrapping the callback in expectAsync() does. But it obscures the purpose of the test.

The async nature of this particular test is not integral to the functionality. It is merely a necessary evil to ensure that the Polymer element has been updated. Instead, I would much prefer to write:
      test('updates the input', (){
        expect(
          input.value,
          startsWith('First Half: [pepperoni]')
        );
      });
The intent of that test is much clearer.

To make that work, I need a setup that blocks. I had forgotten that vanilla unittest in Dart supports this if the setUp() block returns a Future:
      setUp((){
        input = _container.append(createElement('<input>'));
        syncInputFromAttr(input, _el, 'state');
        _el.model.firstHalfToppings.add('pepperoni');

       var completer = new Completer();
       _el.async(completer.complete);
       return completer.future;
      });
I am none too fond of the completer dance that is necessary. My preference would be to use something along the lines of Ruby's tap(), which in Dart would look something like this:
        return new MyCompleter()
          ..tap((completer){ _el.async(completer.complete); })
          .future;
Alas, Dart does not support tap() or an equivalent. Alas too that Completer is abstract, making it impossible to extend to add this functionality.

When I use the Page Objects testing pattern with my Polymer elements, I typically define a flush() method on the page object. The page object flush() can do the completer dance, making the code pretty nice.

The closest that I can get without page objects is a _flush() test helper:
Future<T> _flush(el) {
  var completer = new Completer();
  el.async(completer.complete);
  return completer.future;
}
This leaves my setup as:
      setUp((){
        input = _container.append(createElement('<input>'));
        syncInputFromAttr(input, _el, 'state');
        _el.model.firstHalfToppings.add('pepperoni');

        return _flush(_el);
      });
That is nice, but ugh, how I hate indirection in tests.

Update: This is one Future method in Polymer: onMutation(). And it works, but there is something of an impedance mismatch as it requires a node argument. I can make the tests pass if I supply the Polymer element's shadow root as the argument:
      setUp((){
        input = _container.append(createElement('<input>'));
        syncInputFromAttr(input, _el, 'state');
        _el.model.firstHalfToppings.add('pepperoni');

        return _el.onMutation(_el.shadowRoot);
      });
While that works, it adds undesirable complexity. Reading that in 6 months I would wonder why I was passing the shadowRoot property—was that a requirement of my test? Of my code? Or was it (as is the case here) just something that made the test work, but has no particular value.

I think I will stick with some variation of the completer dance for now. But it sure would be nice if the Polymer.dart folks would add a proper Future version of async.


Day #5

No comments:

Post a Comment