Tuesday, October 29, 2013

Simple Acceptance Tests in Angular.dart


Up tonight, I hope to test the full stack of my angular-calendar application. Well, maybe not the full stack, but the entire application with some of the HTTP calls stubbed out. It is written in Angular.dart, the Dart port of the exciting AngularJS framework.

Last night, I got unit testing working, thanks to the built-in module() and inject() test helpers. Actually, it was more than unit testing—it was interaction testing. Using the Angular.dart test helpers, I was able to inject two custom written Angular classes and test the interaction of the two. I think that I can build on that to achieve near-acceptance testing.

The actual application module currently injects two application classes—a controller and a backend service:
import 'package:angular/angular.dart';
import 'package:angular_calendar/calendar.dart';
main() {
  var module = new AngularModule()
    ..type(AppointmentBackend)
    ..type(AppointmentController);
  ngBootstrap(module: module);
}
(I have a separate branch with routing, but it is not currently in master)

The unittest setUp() equivalent should be something along the lines of:
  group('Appointment Application', (){
    setUp((){
      setUpInjector();
      module((Module _) => _
        ..type(MockHttpBackend)
        ..type(AppointmentBackend)
        ..type(AppointmentController)
      );
    });
    // Tests will go here...
  });
The built-in setUpInjector() helper method prepares the testing environment for Angular's dependency inject goodness. The module() helper creates a DI module, with some testing support already injected. Then I used the DI package's type() to inject three classes by type: the two application classes and a Angular.dart's MockHttpBackend, which is used to stub out HTTP calls.

That setUp() block works just fine. Except it does not actually do anything. As I did last night, I could use the inject() test helper to inject instances of one or more of those classes into a test. But that won't help me write an acceptance test along the lines of “if the user hits enter in the new appointment text field, it should be added to the UI.” At most I can say that, if the controller's add() method is invoked, then the appointments instance variable should include a new record. That is a very valuable test (and one that I learned how to write last night), but not useful for acceptance tests.

For acceptance tests in Angular.dart, I need a TestBed. Like their AngularJS counterparts, the Angular.dart team is going out of the way to make application testing robust and powerful. The primary tools include the already seen module() and inject() helper method and the new TestBed class. Since the test bed is the thing being testing, it gets inject()-ed after the module() setup:
  group('Appointment Application', (){
    TestBed tb;
    setUp((){
      setUpInjector();
      module((Module _) => _
        ..type(MockHttpBackend)
        ..type(AppointmentBackend)
        ..type(AppointmentController)
      );
      inject((TestBed _) => tb = _);
    });
    // Tests will go here...
  });
Here, it might be better to test my routing branch, because the HTML that needs to be compiled with my Angular module has a smaller footprint. For now, I stick with the same largish template that exists on the application's index.html file. It gets compiled into the test bed in the acceptance test:
    test('Retrieves records from HTTP backend', (){
      tb.compile('''
    <div appt-controller>
      <ul class="unstyled">
        <li ng-repeat="appt in day.appointments">
          {{appt.time}} {{appt.title}}
          <a ng-click="day.remove(appt)">
            <i class="icon-remove" ></i>
          </a>
        </li>
      </ul>
      <form onsubmit="return false;" class="form-inline">
        <input ng-model="day.newAppointmentText" type="text" size="30"
               placeholder="15:00 Learn Dart">
        <input ng-click="day.add()" class="btn-primary" type="submit" value="add">
      </form>
    </div>''');

      // Expectations will go here...
    });
If I run the test as-is, I get a failure:
Uncaught Error: [Unexpected request: GET /appointments
No more requests expected]
Nice! So my test, which does not actually have any expectations, fails because my mock HTTP backend saw an HTTP request that it was not told about. Already I am testing some pretty full-stack stuff: that the controller attaches to this view template and makes an HTTP request to populate the template.

To change the message, I can stub the request with whenGET():
    test('Retrieves records from HTTP backend', (){
      inject((TestBed tb, HttpBackend http) {
        http.
          whenGET('/appointments').
          respond(200, '[{"id:"42", "title":"Test Appt #1", "time":"00:00"}]');

        tb.compile(/* ... */);
      // Expectations will go here...
    });
And that works! The test "passes" because I have successfully stubbed out the initial HTTP requests.

I am not quite done, however. I would like to see the response populate the view. I would hope that something like this would work:
    test('Retrieves records from HTTP backend', (){
      inject((TestBed tb, HttpBackend http) {
        http.
          whenGET('/appointments').
          respond(200, '[{"id:"42", "title":"Test Appt #1", "time":"00:00"}]');
        tb.compile(/* ... */);
        var element = tb.rootElement.query('ul');
        expect(element.text, contains('Test Appt #1'));
      });
    });
But I am unable to get the mock HTTP's future to complete under my test bed. If I try to print from the future in the application code:
class AppointmentBackend {
  Http _http;
  AppointmentBackend(this._http);
  init(AppointmentController cal) {
    return _http(method: 'GET', url: '/appointments').
      then((HttpResponse res) {
        print("res: ${res.responseText}");
        return cal.appointments = res.data;
      });
  }
  // ...
}
I never see the print() statement output. The return type of _http() is a Future—it is just not being completed.

Ah well, fodder for tomorrow. Even without that working fully, this test bed stuff seems extremely promising.


Day #919

No comments:

Post a Comment