Monday, May 20, 2013

Dart Test Helpers

‹prev | My Chain | next›

I find myself repeating a lot of the same test actions in my ICE Code Editor test suite. This seems a fine opportunity to explore test helpers in Dart unit tests.

The one in particular that I find myself doing a lot is clicking on element, usually with particular content:
      queryAll('button').
        firstWhere((e)=> e.text=='☰').
        click();
My first instinct is that the helpers should be a separate library that is imported into my test suite. The main reason is that I can import with an “as” prefix to make it patently obvious where the helper methods live. So, in my main ice_test.dart main test file, I add the import statement:
library ice_test;

import 'package:unittest/unittest.dart';
// ...
import 'helpers.dart' as helpers;
import 'package:ice_code_editor/ice.dart';

main(){
  // tests go here...
}
In helpers.dart, I start with a single click() function:
library ice_test_helpers;

import 'dart:html';

void click(String selector, {text}) {
  if (text == null) return query(selector).click();

  queryAll(selector).
    firstWhere((e)=> e.text==text).
    click();
}
The click function requires a string selector that will be used to query for elements to click. If no text is specified—if the optional, named parameter text is null—then I query for the first matching selector and click it. If the text parameter is specified, then I query for all matching selectors, find the first that contains the supplied text and click that.

I continue to use the firstWhere() because it will throw an exception if no matching element is found. I may want to bundle that into a new exception that makes it more obvious what has gone wrong in the test, but I leave it for now.

With that, I can change the test that verifies one of the ways to close a menu:
    test("the menu button closes the projects dialog", (){
      queryAll('button').
        firstWhere((e)=> e.text=='☰').
        click();

      queryAll('li').
        firstWhere((e)=> e.text=='Projects').
        click();

      queryAll('button').
        firstWhere((e)=> e.text=='☰').
        click();

      expect(
        queryAll('div').map((e)=> e.text).toList(),
        isNot(contains(matches('Saved Projects')))
      );
    });
Instead, I can write that as:
    test("the menu button closes the projects dialog", (){
      helpers.click('button', text: '☰');
      helpers.click('li', text: 'Projects');
      helpers.click('button', text: '☰');

      expect(
        queryAll('div').map((e)=> e.text).toList(),
        isNot(contains(matches('Saved Projects')))
      );
    });
Holy clearer intent Batman! It is much easier to see that this test clicks the menu button, then the Projects menu item, then the menu button again.

I might have omitted the “helpers” prefix from my import statement and thus been able to treat click() as a top-level function. I tend to think that the prefix will aid in long-term maintainability of the test suite as there will never be a question as to the source of the helper function.


Day #757

No comments:

Post a Comment