Tuesday, May 21, 2013

Custom Test Matchers in Dart

‹prev | My Chain | next›

After last night, I have helpered my way to a nice looking Dart test:
    test("clicking the project menu item opens the project dialog", (){
      helpers.click('button', text: '☰');
      helpers.click('li', text: 'Projects');

      expect(
        queryAll('div').map((e)=> e.text).toList(),
        contains(matches('Saved Projects'))
      );
    });
That's downright pretty, except… that expect() is, let's face it, plain yucky.

The thing that I am testing is kinda OK. It is a map of the text contents of <div> elements. I could do without the map(), but it's not horrible. The toList() is bothersome. It can be omitted, but it is useful to have around. For instance, if I cause an intentional failure by naming the menu "Saved Code" instead of "Saved Projects", I get a nice error message that gives me an idea of what went wrong:
FAIL: project menu clicking the project menu item opens the project dialog
  Expected: contains match 'Saved Projects'
       but: was <[☰X
          Saved Code
          , ☰, X, , , , , , , , , , , , , X, , , , 
          Saved Code
          ]>.
If I omit the toList(), then the object that I am testing is an Iterable. It still passes or fails as desired, but the failure message is less nice:
FAIL: project menu clicking the project menu item opens the project dialog
  Expected: contains match 'Saved Projects'
       but: was <Instance of 'MappedListIterable':554001401>.
So I leave toList() as a bit of test code ugliness so that I get nicer test code output. I can live with that.

The expected value is not quite as nice. Dart provides a nice matcher for lists: contains(). So in this case I am expecting a list that contains something. That something is anything that matches the string 'Saved Projects'. It works, but it is hard to read.

But hard to read matchers are what custom matchers are for. For this expectation, I may not even need to write an entirely new matcher. Instead, I start by inheriting from CustomMatcher. These nifty little things set a description ("List of elements") and a feature name ("Element list content") in the constructor:
class ElementListMatcher extends CustomMatcher {
  ElementListMatcher(matcher) :
      super("List of elements", "Element list content", matcher);

  featureValueOf(elements) => elements.map((e)=> e.text).toList();
}
I then pick a value to extract from the actual value. If the actual value is a list of elements, then the above extracts the text contents in List form. That is just what I had to do by hand in my test, but all of the ugliness of mapping and converting from an iterable to a list is done in the custom matcher.

I then create a top-level helper that uses this matcher to check the extracted/featured list to see if it contains a match:
elements_contain(Pattern content) =>
  new ElementListMatcher(contains(matches(content)));
Those are two pretty simple helpers that let me rewrite my test entirely with helpers as:
    test("clicking the project menu item opens the project dialog", (){
      helpers.click('button', text: '☰');
      helpers.click('li', text: 'Projects');

      expect(
        queryAll('div'),
        helpers.elements_contain('Saved Projects')
      );
    });
That is pretty all the way through: I click a button with the menu icon, I click the "Projects" menu item, then I expect one of the <div> tags to contain the text "Saved Projects". Wonderful!

And best of all it works!

If I again intentionally make my test fail, I get:
FAIL: project menu clicking the project menu item opens the project dialog
  Expected: List of elements contains match 'Saved Projects'
       but: Element list content was <[☰X
          Saved Code
          , ☰, X, , , , , , , , , , , , , X, , , , 
          Saved Code
          ]>.
That is even more expressive than what I had when I manually extracted a list to test and the test content is easier to read. The use of the helpers prefix from last night is the only remaining noise (and I still think it worth keeping about). All in all, these test matchers are pretty powerful.


Day #758

No comments:

Post a Comment