Thursday, September 19, 2013

Crazy Testing of Backbone.js Apps with Dart


You can probably file this post under “bad ideas.” But what the heck, if you can't do a few crazy things when blogging every single day, then what's the point of blogging every day?

Here's the thing: I love testing in Dart and I hate testing in JavaScript. So the obvious question becomes… can I test my JavaScript with Dart?

I start by making the Funky Backbone Calendar from Recipes with Backbone into a Dart project. This is accomplished simply by adding a pubspec.yaml file naming my project and listing the dependencies:
name: funky_calendar
dependencies:
  unittest: any
  plummbur_kruk: any
  intl: any
  js: any
Since I am testing, I need unittest. Dart does not facilitate any way to stub requests, so I will use a “real fake” server (plummbur_kruk) to test my Backbone code. The intl is needed because Dart no longer supports converting dates to ISO 8601 strings (sigh). Finally, I will need the js-interop package to interact with the JavaScript code being tested.

I install my dependencies:
➜  calendar git:(dart-testing) ✗ pub install
Resolving dependencies..................
Dependencies installed!
Now I add the test context web page:
<html>
<head>
  <title>Funky Calendar Test Suite</title>

  <!-- Library files -->
  <script type="text/javascript" src="../public/javascripts/jquery.min.js"></script>
  <script type="text/javascript" src="../public/javascripts/jquery-ui.min.js"></script>
  <script type="text/javascript" src="../public/javascripts/underscore.js"></script>
  <script type="text/javascript" src="../public/javascripts/backbone.js"></script>

  <!-- include source files here... -->
  <script type="text/javascript" src="../public/javascripts/calendar.js"></script>

  <!-- Dart tests... -->
  <script type="application/dart" src="test.dart"></script>

  <script type='text/javascript'>
    var testRunner = window.testRunner || window.layoutTestController;
    if (testRunner) {
      function handleMessage(m) {
        if (m.data == 'done') {
          testRunner.notifyDone();
        }
      }
      testRunner.waitUntilDone();
      window.addEventListener("message", handleMessage, false);
    }
  </script>
  <script src="packages/browser/dart.js"></script>
</head>

<body>
</body>
</html>
It is probably time to move that testRunner code at the bottom into a package. It prevents tests from completing until the suite signals that all have been run. Actually, I hope that unittest will make that work soon-ish. The rest of the HTML loads the Backbone and associated JavaScript code, along with the test.dart file.

As for test.dart, I copy the skeleton from various other projects. It has a main() entry point that will run my tests. It also polls for all of the tests being done, communicating back to the testRunner in the web page context:
library funky_calendar_test;

import 'package:unittest/unittest.dart';
import 'package:plummbur_kruk/kruk.dart';

import 'dart:html';
import 'dart:async';

import 'package:intl/intl.dart';

final iso8601 = new DateFormat('yyyy-MM-dd');

main() {
  // tests will go here...

  pollForDone(testCases);
}

pollForDone(List tests) {
  if (tests.every((t)=> t.isComplete)) {
    window.postMessage('done', window.location.href);
    return;
  }

  var wait = new Duration(milliseconds: 100);
  new Timer(wait, ()=> pollForDone(tests));
}
For my first test, I will populate the backend REST-like interface in plummbur-kruk and expect that the record shows up in the Backbone app:
    var el;

    setUp((){
      document.head.append(new BaseElement()..href = Kruk.SERVER_ROOT);;

      el = document.body.append(new Element.html('<div id=calendar>'));

      var today = new DateTime.now(),
          fifteenth = new DateTime(today.year, today.month, 15),
          doc = '''
            {
              "title": "Get Funky",
              "description": "asdf",
              "startDate": "${iso8601.format(fifteenth)}"
            }''';

      return Future.wait([
        Kruk.create(doc),
        Kruk.alias('/widgets', as: '/appointments')
      ]);

    });

    tearDown((){
      el.remove();
      return Kruk.deleteAll();
    });

    test("populates the calendar with appointments", (){
      
    });
I run the test suite with a familiar Bash script:
#!/bin/bash

# Start the test server
packages/plummbur_kruk/start.sh

# Run a set of Dart Unit tests
results=$(content_shell --dump-render-tree test/index.html 2>&1)
echo -e "$results"

# Stop the server
packages/plummbur_kruk/stop.sh

# check to see if DumpRenderTree tests
# fails, since it always returns 0
if [[ "$results" == *"Some tests failed"* ]]
then
    exit 1
fi

echo
echo "Looks good!"
And the test actually passes:
➜  calendar git:(dart-testing) ✗ ./test/run.sh
Server started on port: 31337
[2013-09-19 23:52:12.093] "POST /widgets" 201
[2013-09-19 23:52:12.132] "POST /alias" 204
[2013-09-19 23:52:12.140] "DELETE /widgets/ALL" 204
CONSOLE MESSAGE: unittest-suite-wait-for-done
CONSOLE MESSAGE: PASS: the initial view populates the calendar with appointments
CONSOLE MESSAGE: 
CONSOLE MESSAGE: All 1 tests passed.
CONSOLE MESSAGE: unittest-suite-success
...

Looks good!
Of course, my test is currently empty so I can't quite claim victory yet. And here, I see why this might not be a great idea. Since I have to wait for the JavaScript files to load and be evaluated, I add a delay before my test:
    test("populates the calendar with appointments", (){
      new Timer(
        new Duration(milliseconds: 500),
        expectAsync0((){
          var foo = new js.Proxy(js.context.Cal, query('#calendar'));
          js.context.Backbone.history.loadUrl();
        })
      );
    });
In addition to making that uglier, it still fails to work. I am getting weird js-interop isolate errors:
FAIL: the initial view populates the calendar with appointments
  Caught The null object does not have a method 'callSync'.
  
  NoSuchMethodError : method not found: 'callSync'
  Receiver: null
  Arguments: [GrowableObjectArray len:0]
  dart:core-patch/object_patch.dart 20:25                                                                                                   Object.noSuchMethod
  package:js/js.dart 756:35                                                                                                                 _enterScope
  package:js/js.dart 735:28                                                                                                                 _enterScopeIfNeeded
  package:js/js.dart 724:22                                                                                                                 context
  ../../test.dart 50:37       
I am not quite sure what is going on here. That error, of course, is because I forgot to include the packages/browser/interop.js script for js-interop from the testing context page. Things still don't quite work even with that, but I'm getting closer.

But this seems promising enough to continue tomorrow. Well, at least fun enough.


Day #879


No comments:

Post a Comment