Tuesday, May 13, 2014

Thankfully, Acceptance Tests


I may have a solution to the problem of how best to introduce MVC in Dart for Hipsters.

I am in the process of another rewrite and have found—thanks to some super helpful errata reports—that things are amiss in the chapter that introduces MVC. Unfortunately, this is an important chapter, so problems in it are more worrisome than in other chapters. It is the bridge chapter between intro-to-Dart material and really putting Dart through its paces. But if this chapter is wonky, the chapters that fully explore Dart's power are harder to understand—or worse, the reader gets frustrated and drops the book altogether. So I need to fix this.

Last night's proposed solution looks something like this:
main() {
  var comics_view, comics_collection;

  comics_collection = new ComicsCollection(
    onChange: ()=> comics_view.render()
  );
  comics_view = new ComicsView(
    el: document.query('#comics-list'),
    collection: comics_collection
  );
  comics_collection.fetch();

  // ...
}
Clearly, things can be improved, but that's rather the point of a middle-of-the-book chapter. The onChange callback hints at callback hell that will never arise thanks to Darts awesome streams. Still, it ought to make some sense to the reader—when the collection changes, the view needs to re-render. The view is attached to an element with the same comic book “collection.” The collection is a collection of models. I have views, collections and models—an MVC-like solution.

What I like most about this solution is that it works (always important)—I can run this application code in a browser to add and remove comic books from a REST-like backend:



So tonight, I set out to fix my tests. Surely my test must be failing because I removed a bunch of event-based code that was the source of many an errata. And indeed they are failing, but only because I changed the code location. Once the tests are updated to reference the new location of the MVC code, everything passes:
➜  code git:(master) ✗ ./test_runner.sh
CONSOLE MESSAGE: PASS: [skeleton] the skeleton app returns OK
CONSOLE MESSAGE: PASS: [main] the app returns OK
CONSOLE MESSAGE: PASS: [main] populates the list
CONSOLE MESSAGE: PASS: [numbers] 2 + 2 = 4
...
CONSOLE MESSAGE: PASS: [canvas] player is drawn
CONSOLE MESSAGE:
CONSOLE MESSAGE: All 163 tests passed.

Static type analysis...
Analyzing test.dart... 

Looks good!
My first reaction to all of my tests passing is that of the great Frozone: “Aw, now that ain't right.”

It turns out that my tests are more of the acceptance test variety:
    test('populates the list', (){
      Main.main();
      expect(el.innerHtml, contains('Sandman'));
    });
In other words, the tests are less brittle than unit tests which might have tested the under-the-covers implementation. To make sure that I am not seeing a false positive, I intentionally choose an invalid expectation, which causes my test to fail:
CONSOLE MESSAGE: FAIL: [main] populates the list
  Expected: contains 'Sandmän'
    Actual: '      <li id="cd46d1d0-b420-11e3-a31b-29d8b92ba7e6">
'
    '        Sandman
'   
    '        (Neil Gaiman)
'   
    '        <a class="delete">[delete]</a>
'   
    '      </li>'
To leave the code a little better than I found it, I opt to write another test. This one will click on the add-comic element, fill out the form for a Superman comic, submit the form, and expect that the Superman comic is now included on the list:
    group('adding', (){
      setUp((){
        Main.main();
        query('#add-comic').click();
        query('input[name=title]').value = 'Superman';
        query('input[value="Create!"]').click();
      });

      test('populates the list', (){
        expect(el.innerHtml, contains('Superman'));
      });
    });
That does not quite work because I am not allowing for form submission and page-update-on-success. To accommodate those, I add a small delay to the setup block:
    group('adding', (){
      setUp((){
        Main.main();
        query('#add-comic').click();
        query('input[name=title]').value = 'Superman';
        query('input[value="Create!"]').click();

        var completer = new Completer();
        new Timer(
          new Duration(milliseconds: 20),
          completer.complete
        );
        return completer.future;
      });

      test('populates the list', (){
        expect(el.innerHtml, contains('Superman'));
      });
    });
If this were written with scheduled_test, I could make that a little prettier, but it is not horrible as is. Returning a future from setUp() tells unittest that it should wait for the future to complete before running any tests in the group. In this case, I complete the completer after a simple 20 milliseconds delay. With that, I have 164 passing tests and assurance that the proposed code changes to the MVC chapter are behaving as needed.

So I seem to be in pretty good shape. Now I just need to adapt my narrative to fit the new code…


Day #62

1 comment:

  1. Thanks for the great effort. But I'm starting to think that Dart is not so good. They made too many errors both in architecture and in usability. It's a mess. GWT was much better designed.

    ReplyDelete