Wednesday, May 22, 2013

Driving Dart Integration Tests

‹prev | My Chain | next›

The crazy thing about driving things with tests is that stuff gets done. I have been having a good old time playing around with the test suite in the Dart version of the ICE Code Editor. I spent so much time fiddling with the tests that I barely noticed the progress with functionality. Mostly thanks to #pairwithme sessions, the functionality of the ICE Code Editor is really coming along.

Tonight, I take a step back to see how it is working with the new test helpers that I have written over the past two nights when driving new functionality. Thanks to last night's #pairwithme session with Santiago Arias, ICE now lists the currently saved projects in the Projects menu. Tonight I would like to be able to click on old ones to make them active.

I start with a long UI workflow test:
    test("click names to switch between projects", (){
      helpers.click('button', text: '☰');
      helpers.click('li', text: 'New');

      query('input').value = 'Project #1';
      helpers.click('button', text: 'Save');

      editor.content = 'Code #1';
      helpers.click('button', text: '☰');
      helpers.click('li', text: 'Save');

      helpers.click('button', text: '☰');
      helpers.click('li', text: 'New');

      query('input').value = 'Project #2';
      helpers.click('button', text: 'Save');

      editor.content = 'Code #2';
      helpers.click('button', text: '☰');
      helpers.click('li', text: 'Save');

      helpers.click('button', text: '☰');
      helpers.click('li', text: 'Projects');
      helpers.click('li', text: 'Project #1');

      expect(
        editor.content,
        equals('Code #1')
      );
    });
This fully exercises the full screen ICE menus by clicking the menu button (the UTF-8 ☰ symbol), then creating a new project, setting the content and saving the project. I do that twice, at which point the content for the second project is still active. I then open the Projects menu and select the first project from the list. The expectation is that the editor content will have changed to “Code #1” the content of the first project.

It is so cool that, thanks to Dart's testing facilities and some simple helpers, the test is so expressive and yet completely functional. The first time I run that test, I get a failure. The exact failure that I had hoped to get to drive this feature:
FAIL: project menu click names to switch between projects
  Expected: 'Code #1'
       but: was 'Code #2'.
How awesome is that?

Then again, maybe my UI testing expectations are just too low. Perhaps this is really how it ought to be. Regardless, this is reality in the world of Dart and I embrace it by writing the code that implements this. With that bit of smugness fully internalized, I run into all sorts of problems.

The implementation is not too hard:
    _store.forEach((title, data) {
      var project = new Element.html('<li>${title}</li>')
        ..onClick.listen((e)=> _openProject(title))
        ..onClick.listen((e)=> _hideMenu());

      menu.query('ul').children.add(project);
    });
For each item in the data store, I create a menu item for the Projects menu. Each project menu item gets a couple of handlers to open the project and hide the menu. The underlying Store class could support opening a project better, but that is not really the problem.

The problem is that, after saving items by clicking the “Save” option from the main menu, I was not closing the main menu. It would stay up causing subsequent tests to fail because the wrong menus were now active.

I could have written this test such that the localStorage store was initialized with projects in place. This would have avoided the save-not-closing-the-menu issue (though I would not have found the bug otherwise) and the subsequent implementation would likely end up exactly the same. But there is something exciting about a test that exercises the full stack in milliseconds. If I had other tests that covered similar ground, I might manually initialize the localStorage, but I really prefer to keep this around.

It turns out to be quite the effort to keep it around. The test continues to fail and is compounded with a problem running these tests solo due to js-interop concerns. Thankfully, tonight's #pairwithme pair, Daniel Gempesaw, helps me through it.

We drive the problem away with another test:
    test("clicking save closes the main menu", (){
      helpers.click('button', text: '☰');
      helpers.click('li', text:  'Save');

      expect(queryAll('li').map((e)=> e.text).toList(), isEmpty);
    });
All that is needed to make that pass is a second on-click listener for the “Save” menu item:
  Element get _saveMenuItem {
    return new Element.html('<li>Save</li>')
      ..onClick.listen((e)=> _save())
      ..onClick.listen((e)=> _hideMenu());
  }
With that, not one, but two tests are passing. More importantly, it is now possible to switch between projects in the Dart version of the ICE Code Editor. Yay!


Day #759

2 comments:

  1. Hey, thanks for the #pairwithme session last night! I enjoyed it and it's got me excited about Dart and ICE. Thinking about it, pairing is a really good way to generate interest in an open-source project, although it might not scale very well...

    ReplyDelete
    Replies
    1. Thanks for the help! I'm always surprised when I run into problems with things that I think ought to be easy. It's a bit annoying that this one was self-inflicted (not closing the menu after clicking save!). Still, much thanks for getting me through it.

      I think you're right about this being a good way to build interest in a project. ICE isn't the ideal project for that since development will slow once it reaches feature complete. When I'm ready to write the Great American Framework™, I'll publicize it like this :)

      Delete