Saturday, September 21, 2013

The Neighbor's Shoe


What could be finer that one Dart unit test of a Backbone.js application?

Yup, two.

I remain skeptical of this overall approach. Testing a JavaScript application with Dart is crazy, right? Well, maybe not too crazy. Dart's built-in unittest library is pretty awesome and there are already some supporting libraries sprouting up to make it even nicer. Aside from the inability stub HTTP requests, Dart testing is superior to any JavaScript testing solution. I do not believe that to be an exaggeration.

Anyhow, I am going to start by duplicating my one test rather than creating a second useful test. This will minimize the moving parts, helping to isolate problems. The working test is:
    test("populates the calendar with appointments", (){
      new js.Proxy(js.context.Cal, query('#calendar'));

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

      new Timer(
        new Duration(milliseconds: 10),
        expectAsync0((){
          expect(cell.text, matches("Get Funky"));
        })
      );
    });
It uses Dart's js-interop to instantiate the Backbone application (Cal). It looks through all of the calendar elements on the page for the 15th of the month and finally checks the expectation that the appropriate “funky” appointment is on the calendar. From last night, the 10 millisecond delay was the magic sauce that allowed the application to query the test backend server, drawing the results on the screen.

I already have a teardown function that removes the Backbone application and associated elements. So I make a copy of the test, changing only the test description by adding “(copy)”:
    test("populates the calendar with appointments (copy)", (){
      // Exact same test here...
    });
Naturally that fails:
PASS: the initial view populates the calendar with appointments
FAIL: the initial view populates the calendar with appointments (copy)
  Expected: match 'Get Funky'
    Actual: '15'
C'mon Dart! What gives?

Well, maybe this isn't Dart. Instead, there is the funky nature of Backbone URL routing that requires routing history to be started (to manage push state and the like). My Backbone application does that:
  // ...
  new Routes({application: application});
  try {
    Backbone.history.start();
  } catch (x) {
    console.log(x);
  }
  // ...
And it works just fine when the application is run once. Under normal circumstances, running once is just fine—a Backbone application does not need to start twice in a browser. Under test however, Backbone applications get run many, many times. Or twice when you're messing around with Dart testing. This is the reason for the try-catch in my existing application—to prevent exceptions under test when Backbone.history.start() is called more than once.

That's all well and good, but if I am going to test an application that relies on history—as this one does—I need some way to reset history each time the application is instantiated. In my Jasmine tests, I had been doing this with Backbone.history.loadUrl(). In Dart, I can access that method via the main context proxy:
    test("populates the calendar with appointments (copy)", (){
      js.context.Backbone.history.loadUrl();
      new js.Proxy(js.context.Cal, query('#calendar'));
      // ...
    });
But when I try the tests now, I get a very unhelpful failure:
ERROR: the initial view populates the calendar with appointments (copy)
  Test failed: Caught TypeError: Cannot call method 'replace' of undefined undefined:1
Here, I think, is a definite problem with using Dart to test JavaScript—the error message stinks. There is no stack trace. The line number—undefined:1—is useless. I have to infer where the problem is based on the message alone.

I would remind readers at this point that no one thinks using Dart to test JavaScript applications is a good idea. None of this should be taken as an indictment of Dart, Dart testing or Backbone. What I am doing is silly. But sometimes, you find useful stuff when doing silly things. Anyhow…

I am able to figure out the problem eventually. I had gotten the order of Backbone.history.loadUrl() and instantiation of my application wrong. Specifically, the URL needs to be performed after the application is running:
    test("populates the calendar with appointments (copy)", (){
      new js.Proxy(js.context.Cal, query('#calendar'));
      js.context.Backbone.history.loadUrl();
      // ...
    });
With that, I have two Dart tests passing that describe my Backbone application:
unittest-suite-wait-for-done
PASS: the initial view populates the calendar with appointments 
PASS: the initial view populates the calendar with appointments (copy)
All 2 tests passed.
unittest-suite-success
The loadUrl() order error was a pretty silly mistake to make. I could have just as easily made the same mistake testing in JavaScript that I did in Dart. A more descriptive stack trace or error message would almost certainly have helped me understand the problem sooner.

Still, I love Dart testing enough that I am not 100% ready to drop the idea of using Dart to test JavaScript applications. I admit that I am a dog with the neighbor's shoe here. I should not have the shoe and I know this. But man, is it fun to rip into it. So I'll probably continue gnawing on it tomorrow.


Day #881

No comments:

Post a Comment