Wednesday, October 30, 2013

An Ugly (But Legit) Angular.dart Acceptance Test


In my angular-calendar example application, a controller fetches records from the server, then populates the view. The then part of that statement is important tonight. It is important because the then() in my Angular.dart application code is not working in my tests.

The AppointmentController is backed by an AppointmentBackend service. When the controller is created, it calls the init() method in the HTTP backend service to fetch existing records:
class AppointmentBackend {
  // ...
  init(AppointmentController cal) {
    _http(method: 'GET', url: '/appointments').
      then((HttpResponse res)=> cal.appointments = res.data);
  }
}
This works fine in the application:



But I have been unable to get the then() in the service's init() to fire under test:
    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(/* HTML template here */);
        var element = tb.rootElement.query('ul');
        expect(element.text, contains('Test Appt #1'));
      });
    });
This test stubs out HTTP GET requests to /appointments, responding with the "Test Appt #1" appointment. The test bed, which is an Angular.dart thing, should compile HTML, attach controllers and do any normal initialization. And it seems to work as I expect, but only to a point. If the HTTP stub is not present, I get an error. But with it present, I am not seeing the necessary then() callback in the application code. The test fails with:
ERROR: Appointment Application Retrieves records from HTTP backend
  Test failed: Caught Expected: contains 'Test Appt #1'
    Actual: '\n'
    '        \n'
    '      '
But even if I put tracer bullets in the then() callback, it is not being called.

A pub upgrade to the latest version similarly has no effect:
➜  angular-calendar git:(master) ✗ pub upgrade
Resolving dependencies.............................................
Downloading angular 0.0.7 from hosted...
Dependencies upgraded!
So at this point either the testing library is not working properly or it works differently than I expect it should work. Even though this is pre-alpha software, it is more than likely that my understanding is incorrect. Regardless, the approach to getting the test to behave as desired is the same—I need to start poking around in the actual Angular.dart library.

This is Dart, so I need only point my sample application's pubspec.yaml to my local copy of the GitHub repo:
name: angular_calendar
dependencies:
  angular:
    path: ../angular.dart
# ...
And, with a quick pub get to obtain the new dependency, I am ready.

And, eventually, I figure it out. At some point, I ought to record a screencast of how I do this—not because I have some awesome skills or approach, but rather because my approach is such a mess. My success is not based on any real skill. It is sheer will and much blind luck.

What I find is that I need to wait for one reactor loop. Twice. After the first loop, I have to manually invoke the mock HTTP service's response callbacks. This seems to be a way to get the futures involved to complete, which then invoke any then() callbacks. The next wait allows the controller time to assign the mock HTTP updated appointments instance variable. I then have to manually “digest” the template to see the result. The test that does all that is:
    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(/* HTML template here */);

        Timer.run(expectAsync0(() {
          http.responses[0]();

          Timer.run(expectAsync0(() {
            tb.rootScope.$digest();

            var element = tb.rootElement.query('ul');
            expect(element.text, contains('Test Appt #1'));
          }));
        }));
      });
    });
As tests go, that is pretty ugly. I am not a stickler for clean code in tests, so that is not too much of a concern. I can even live with the nested Timer.run() calls (which run the enclosed code on the next reactor loop). I would probably switch to scheduled_tests to clean those up a little, but even as-is, I can live with them.

What makes this ugly is that I reach under the Angular covers. Twice. Calling the first HTTP response just looks weird. I am not even sure what $digest() does—I just found it in some of the internal Angular tests. Hopefully I can find a higher-level way to get the same functionality.

But as ugly as those are, I have a legit acceptance test for my Angular.dart application. That is a fine way to end the day.


Day #920

1 comment:

  1. Consider using async() for your asynchronous tests: https://github.com/angular/angular.dart/blob/master/lib/mock/zone.dart#L144

    Some examples of it in use:
    https://github.com/angular/angular.dart/blob/master/test/core_dom/compiler_spec.dart#L416
    https://github.com/angular/angular.dart/blob/master/test/core_dom/http_spec.dart#L685

    ReplyDelete