Wednesday, July 31, 2013

Dart Scheduled Test and Browser Tests


After last night, I have the ability to run both dart:io (server side) and dart:html (browser) tests from the same script. This will be great for continuous integration—especially for Dart for Hipsters which has significant number of both kinds of tests and needs to run them often to prevent the kinds of language regressions that I now face in updating the book.

I have a good number of server-side tests in a fake test server package that I am developing to support the book. I would like to add a few browser tests as well—both to aid me in ensuring that my fake test server works and to give me a little more practice at writing tests with the Scheduled Test package.

Since my browser tests are going to hit a running HTTP server—my fake test server—my scheduled tests will actually be a little simpler than the server side tests. In the server-side counterparts, the tests needed to schedule starting the server in addition to requests, responses and actually setting test expectations. I still have a bit of test setup to do, but the setup now only concerns itself with clean-up after the test is run. In Scheduled Test, this is done with an onComplete() callback in the setUp() block:
  final String URL_ROOT = 'http://localhost:31337/widgets';

  group("/widgets", (){
    setUp(() {
      currentSchedule.
        onComplete.
        schedule(() {
          return HttpRequest.request('${URL_ROOT}/ALL', method: 'delete');
        });
    });
    // tests here...
  });
The currentSchedule top-level getter is available regardless of whether or not a setup schedule is created. I use it here to schedule a tear-down step: deleting all of the records in the database via a REST-like web interface.

Now I am ready to write an actual test. I schedule the request first. The response is the second scheduled action and, in there, I set the test expectation:
  group("/widgets", (){
    setUp(() { /* ... */ });

    test("POST new records", (){
      var responseReady = schedule(() {
        return HttpRequest.
          request(URL_ROOT, method: 'post', sendData: '{"test": 42}');
      });

      schedule(() {
        responseReady.then((req) {
          var rec = JSON.parse(req.response);
          expect(rec['test'], 42);
        });
      });
    });

  });
The results of HttpRequest.request() in the first schedule is a future that completes with a request instance. By returning it from the scheduled function, the schedule() function also returns the same future. This is then a quick, clean way to communicate the active request into the next schedule—without relying on local variables.

The request is a POST with JSON contents. The expectation is that the JSON contents will be returned from the server. There should also be a record ID returned from the server, but I will test that in a separate test. With that, I have my browser test passing against a real fake server:
➜  plummbur-kruk git:(scheduled_test) ✗ content_shell --dump-render-tree test/index.html
CONSOLE MESSAGE: unittest-suite-wait-for-done
CONSOLE MESSAGE: PASS: /widgets POST new records

CONSOLE MESSAGE: 
CONSOLE MESSAGE: All 1 tests passed.
CONSOLE MESSAGE: unittest-suite-success
The test passes, but content_shell never returns. I think it will time out after a few minutes, but that is not a recipe for a solid continuous integration setup. The problem is in the test/index.html page that provides the browser context for content_shell to run my tests.

In there, I have testing apparatus that instructs the browser virtual machine that it is, in fact, running tests. As such it needs to wait for a signal before it can consider the page rendered:
<html>
<head>
  <title>Plummbur Kruk Test Suite</title>
  <script type="application/dart" src="browser_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>
Until the testRunner object is notified that it is done, the page is not considered fully rendered and content_shell will not exit. Without that testing apparatus, the page will often render before a single test runs, causing content_shell to exit with no useful testing. So I really need something to notify the message event listener when all tests have completed.

With vanilla unit tests in Dart, I have been running a periodic check to see if all of the tests have completed:
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));
}
When every test is complete, then a “done“ message is posted to the window, which the testing apparatus in test/index.html then converts into the necessary notifyDone() call. In vanilla unittest, this works with:
pollForDone(testCases);
But in Scheduled Test, there is no testCases top-level getter.

After digging through Scheduled Test a bit, I have to admit that I am stumped. The best that I can come up with is to add one last schedule after every other group and test:
main() {
  group("/widgets", (){ /* ... */ });
  test("Complete", (){
    schedule((){
      window.postMessage('done', window.location.href);
    });
  });
}
That seems to do the trick, but it feels wrong. Writing a test that is not a test, but rather is just there to support the harness is awkward. This may be a function of using a testing framework—Scheduled Test—that is intended only for the server. At the risk of calling my grapes sour, the real test hardly needs scheduled test and I am unsure that it really benefits much from Scheduled Test. Then again, there is something to be said for the consistency of using the same test framework for both my server-side and browser tests.

I will call it a night here and hope that tomorrow brings some clarity on the issue.


Day #829

No comments:

Post a Comment