Friday, June 14, 2013

Testing Consistency with Types

‹prev | My Chain | next›

When I rewrote the ICE Code Editor the first time, I found myself in a very unsatisfactory state. Everything seemed to work, but I had no real feel for how well. Sure, I could have put it out there and seen how it went, but these were not typical humans using the application—they are mostly kids reading 3D Game Programming for Kids. I very much want to ensure that I do all that I can to keep the barrier to learning as low as possible. Any minor little bug could easily end a newcomer's dalliance with coding—all the more so with kids.

So I, along with many awesome #pairwithme partners, rewrote from scratch in Dart. There are numerous reasons to code in Dart (saner, almost elegant syntax, better documentation support, assurance that comes from static typing, and better cross browser support), but the one that I desired above all others was a strong test suite.

100+ tests later (many of the full stack integration variety), I exude confidence. The night before a big presentation to BmoreJS, I deployed a new beta version and didn't give it a second thought. That is something that I never would have done with the JavaScript version. Tonight, I do something else that I never even bothered with in the JavaScript version: testing for consistency.

There are a couple of bugs open in the issue tracker describing how some ICE menus behave slightly differently than others. In my dynamic coding life, I see people use shared examples to do this kind of thing—to verify common behavior. I think this would be fairly easy to add to my test suite. Before I do, I come to my senses.

Because in statically typed language like Dart, I can, y'know, use types.

The main menu is currently assembled from a series of private getters:
  _showMainMenu() {
    var menu = new Element.html('<ul class=ice-menu>');
    el.children.add(menu);

    menu.children
      ..add(_newProjectDialog)
      ..add(_openDialog)
      // ...
      ..add(_helpDialog);
  }
The private getters vary a little, which accounts for slightly different behavior:
  get _openDialog=>       new MenuItem(new OpenDialog(this)).el;
  get _newProjectDialog=> new MenuItem(new NewProjectDialog(this)).el;
  // ...
  get _helpDialog=>     new HelpDialog(this).el;
So I convert all of them to MenuItem and restrict menu items to things that implement the MenuAction interface (has a name and an open() method). In other words, I am formalizing the Strategy pattern. The MenuItem context:
class MenuItem {
  MenuAction action;
  MenuItem(this.action);
  Element get el {
    return new Element.html('<li>${action.name}</li>')
      ..onClick.listen((e)=> _hideMenu())
      ..onClick.listen((e)=> action.open());
  }
}
And the strategy MenuAction:
abstract class MenuAction {
  String get name;
  void open();
}
When I run the dartanalyzer against my code, I get a bunch of errors because none of the menu items implement this new MenuAction interface:
➜  ice-code-editor git:(master) ✗ dartanalyzer lib/ice.dart
Analyzing lib/ice.dart...
[warning] The argument type 'OpenDialog' cannot be assigned to the parameter type 'MenuAction' 
  (/home/chris/repos/ice-code-editor/lib/full.dart, line 118, col 40)
[warning] The argument type 'NewProjectDialog' cannot be assigned to the parameter type 'MenuAction' 
  (/home/chris/repos/ice-code-editor/lib/full.dart, line 119, col 40)
[warning] The argument type 'RenameDialog' cannot be assigned to the parameter type 'MenuAction' 
  (/home/chris/repos/ice-code-editor/lib/full.dart, line 120, col 40)
...
9 warnings found.
I work through each in turn. Most simply need to implement the interface as they already informally implemented the strategy:
class OpenDialog implements MenuAction {
  get name => "Open";
  open() { /* ... */ }
}
I also convert the troublemakers over to be official MenuActions:
class HelpAction implements MenuAction {
  HelpAction(_);
  get name => "Help";
  open(){ /* ... */ }
}
That gives me a rigidly uniform menu. My tests confirm that the individual dialog behaviors remain working as expected. And dartanalyzer ensures that my menu items are all behaving the same:
➜  ice-code-editor git:(master) dartanalyzer lib/ice.dart 
Analyzing lib/ice.dart...
No issues found.
And since both dartanalyzer and my tests are run under continuous integration from drone.io, I can remain confident that things will still operate as desired even as the codebase continues to rapidly evolve.

Wow. I can't believe I almost implemented shared examples to verify this stuff. That was a close one.


Day #782

No comments:

Post a Comment