Monday, October 21, 2013

Getting Started with Unit Tests in Angular.dart


I am at least as excited to play with testing in Angular.dart as I am to learn about the cool stuff it has planned for needy web developers. And I am very eager to explore the promise of this Dart port of the very excellent AngularJS. But I crave solid testing.

I test for different reasons. I test differently for each of those reasons. But no matter what, I always test.

I use test driven development to build robust applications. If every method is written in response to a test, every method tends to be small, there tend to be fewer of them and coupling between methods and classes tends to be low. Code that tends to have few, small methods with low coupling is better prepared to support new features. It is robust code.

I use acceptance tests to ensure that my applications are maintainable. Acceptance tests describe high-level features, usually from a human perspective. If there are certain features and workflows that are important to the overall health of the application, acceptance tests exercise these feature as a person might. Acceptance tests maintain by quickly identifying when a change breaks features and workflow.

So far in my exploration of Dart code, I have found no way to write acceptance tests for web applications without running a full-blown web server. This is because there is no way to stub out HTTP requests in Dart (e.g. if this exact request is made, then respond with this data). Dart is a dynamic language, but not so dynamic as to allow runtime modifications to a core class. And so I run actual test servers.

But this is not the only way. In fact, AngularJS uses this other way already to do much of its testing. It injects HTTP request objects, which may or may not be the real thing, into components that need them. I have eschewed this approach in my Dart applications because I do not believe that it provides true acceptance testing. But perhaps the combination of Angular application structure and dependency injection would be closer? If nothing else, I get to see another testing approach, so let's give it a whirl.

I start with a simple test page to provide web context for my tests, test/index.html:
<script type="application/dart" src="test.dart"></script>
<script src="packages/unittest/test_controller.js"></script>
The test_controller.js script in unittest still seems to be a work-in-progress (or I haven't got it 100% sussed), but it establishes a message listener which I can complete by sending a message once all tests have completed. Thus, my skeleton test.dart is:
import 'package:unittest/unittest.dart';
import 'dart:html';

main(){
  pollForDone(testCases);
}

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

  var wait = new Duration(milliseconds: 100);
  new Timer(wait, ()=> pollForDone(tests));
}
Since many tests are asynchronous, it it necessary to poll like this to ensure that all tests really have completed before telling test_controller.js that testing has completed. Actually, there is probably something in unittest that does this for me—I'll have to find it another day.

I am unsure of the Angular.dart Way™ of testing, so I am going to start small here. I will try to unit test that adding a record in my calendar's appointment controller instructs the server to add a record. The AngularJS Way™ is to inject mock $httpBackend and $controller services (and anything else that is needed). I think I can do that manually (and I'll try that tomorrow). For now, however, I am going to settle for supplying a mock ServerCtrl:
import 'package:unittest/unittest.dart';
import 'package:unittest/mock.dart';
import 'package:angular_calendar/calendar.dart';

class ServerCtrlMock extends Mock implements ServerCtrl {
  Http _http;
  ServerCtrl(Http this._http);
}

main(){
  group('Appointment controller', (){
    var server;
    setUp((){
      server = new ServerCtrlMock();
    });

    test('adding records to server', (){
      var controller = new AppointmentCtrl(server);
      controller.newAppointmentText = '00:00 Test!';
      controller.add();

      server.
        getLogs(callsTo('add', {'time': '00:00', 'title': 'Test!'})).
        verify(happenedOnce);
    });
  });
}
I supply the mock server controller to the AppointmentCtrl constructor. The test then sets the new appointment text, as the Angular view would, then I invoke the add() method, as the ng-click directive in the view would. This is definitely not an acceptance test with all the direct method calls. Still, hopefully this can help drive a small implementation.

The actual expectation in this test is done by checking the logs of the server mock. In this case, I verify that the server's own add() method is invoked once with the string parsed out into the Map shown.

And, after I replace the HttpRequest code in my application:
void add() {
    var newAppt = fromText(newAppointmentText);
    appointments.add(newAppt);
    newAppointmentText = null;
    // HttpRequest.
    //   request(
    //     '/appointments',
    //     method: 'POST',
    //     sendData: JSON.encode(newAppt)
    //   );
    _server.add(newAppt);
  }
My test passes.

Well, that is not quite what I hoped to accomplish tonight, but it is a start. Tomorrow, I am going to fiddle with the mock HTTP backend supplied by Angular.dart. And then maybe I can figure out how to injection is supposed to work in Angular.dart tests.


Day #911

No comments:

Post a Comment