Friday, September 20, 2013

Insanity Day 2: Testing Backbone with Dart


Sometimes you just have to try crazy stuff. Yesterday I added Dart unittest support into my Backbone.js-powered calendar application.

Today, I start with a failing test:
    test("populates the calendar with appointments", (){
      // js.context.Backbone.history.loadUrl();
      new js.Proxy(js.context.Cal, query('#calendar'));

      var _id = iso8601.format(fifteenth);
      var cell = queryAll('td').
        where((el)=> el.id == _id).
        first;

      print('Contents (${_id}): ${cell.text}');
      expect(cell.text, matches("Get Funky"));
    });
In there, I create an instance of the Backbone application, Cal, via Dart's js-interop. Then find the 15th of the month in the generated calendar. Finally, I check my expectations—that the calendar contains the correct, “funky” appointment.

Amazingly, that test actually runs:



Sure, it fails, but it fails in a useful way—with the expectation that a record that should exist on the 15th of the month is not on the Backbone calendar.

Looking closer at the network tab of Chrome's Developer tools, it seems that my “real fake” test server is missing a CORS allowed request header:



That must be a jQuery (used by Backbone) thing. Since my test is running from a file:// URL and my “real fake” test server is running on port 31337 of my localhost, I need that header. Fortunately, that is easy enough—made easier since I am using Dart, naturally. I switch my project from depending on the published version of plummbur_kruk (the “real fake” server), to my local repository:
name: funky_calendar
dependencies:
  unittest: any
  plummbur_kruk:
    path: /home/chris/repos/plummbur-kruk
  intl: any
  js: any
After a quick pub update, my Dart packages are switched over. In my local plummbur_kruk, I add another Access-Control-Allow-Headers:
      HttpResponse res = req.response;
      res.headers
        ..add('Access-Control-Allow-Origin', 'null')
        ..add('Access-Control-Allow-Headers', 'Content-Type')
        ..add('Access-Control-Allow-Headers', 'X-Requested-With')
        ..add('Access-Control-Allow-Methods', 'GET,DELETE,PUT');
Now, when I run my test, I get just the test failure and no network errors:
FAIL: the initial view populates the calendar with appointments
  Expected: match 'Get Funky'
    Actual: '15'
This turns out to be a simple matter of timing. The expectations in my test are being checked before the Backbone application has a change to layout all of its elements. The easiest fix is to add a minimal delay before checking the expectation:
    test("populates the calendar with appointments", (){
      // js.context.Backbone.history.loadUrl();
      new js.Proxy(js.context.Cal, query('#calendar'));

      var _id = iso8601.format(fifteenth);
      var cell = queryAll('td').
        where((el)=> el.id == _id).
        first;

      print('Contents (${_id}): ${cell.text}');

      new Timer(
        new Duration(milliseconds: 10),
        expectAsync0((){
          expect(cell.text, matches("Get Funky"));
        })
      );

    });
And with that, I actually have a passing test:
unittest-suite-wait-for-done
Contents (2013-09-15): 15
PASS: the initial view populates the calendar with appointments

All 1 tests passed.
unittest-suite-success
The tests pass and (if I comment out the teardown), I can even see the unstyled calendar on the test page:



Insanely enough, I can run a legitimate test for Backbone code with Dart unit testing. The benefits are obvious: easier setup, better support for running under continuous integration, nicer test syntax, better async support. There are more questions than answers at this point, but I'm pretty sure I have the makings of another recipe for Recipes with Backbone on my hands.

I kid! Or do I…?

Day #880

No comments:

Post a Comment