Thursday, October 31, 2013

Beautiful Acceptance Tests in Angular.dart


I ended yesterday with an Angular.dart acceptance test that looks like:
    test('Retrieves records from HTTP backend', (){
      inject((TestBed tb, HttpBackend http) {
        // 1. Stub HTTP responses
        http.
          whenGET('/appointments').
          respond(200, '[{"id":"42", "title":"Test Appt #1", "time":"00:00"}]');

        // 2. Apply all Angular directive to supplied HTML
        tb.compile(HTML);

        // 3. Wait a tick, then execute the first response
        Timer.run(expectAsync0(() {
          http.responses[0]();

          // 4. Wait a tick, then “digest” things
          Timer.run(expectAsync0(() {
            tb.rootScope.$digest();

            // 5. Expect that the test appointment is in the DOM now
            var element = tb.rootElement.query('ul');
            expect(element.text, contains('Test Appt #1'));
          }));
        }));
      });
    });
That is not too bad, but I think there is room for improvement.

I start with #3 and “executing” the response. That struck me as odd, but while I was digging through the underbelly of Angular.dart's MockHttpBackend, it seemed necessary. I found that the callbacks that would trigger Future completion in my application code were stored in these responses and so I manually invoked them. Having some time to review, I now see that this is what flush() does.

In fact, I found it odd that I could not get flush() to work previously. I gave up on it when all that it gave me was “No pending request to flush !” errors. It turns out that the “Wait a tick” was the important piece necessary to allow the HTTP responses to be in a position to be flushed. So I change #4 above to:
        Timer.run(expectAsync0(() {
          http.flush();
The other thing that I found off-putting last night was the call to $digest() on the root scope of the module. After digging through the Angular.dart library a bit today, I believe that is necessary. It manually forces bound variables (like the list of appointments) to trigger view updates. Angular.dart has a moderately complex algorithm to determine when to do this in a live application. Invoking $digest() in test speeds things along.

So flush() and $digest() are (I think) legitimate things in an Angular.dart acceptance test. The one other thing that I do to improve the test is to call in the scheduled_test library. I update the development dependencies in pubspec.yaml:
name: angular_calendar
dependencies:
  angular: any
  # ...
dev_dependencies:
  scheduled_test: any
Then, I pub get the new dependency.

The scheduled_test package is unittest with some nice asynchronous support. The only incompatibility with unittest is the lack of a tearDown(). Instead of a tearDown(), I have to schedule what to do when the current test is complete. In this test, that means that I need to tear down the test injector:
    setUp((){
      setUpInjector();
      module((Module _) => _
        ..type(MockHttpBackend)
        ..type(AppointmentBackend)
        ..type(AppointmentController)
        ..type(TestBed)
      );

      currentSchedule.onComplete.schedule(tearDownInjector);
    });
The rest of the setUp() is exactly as it had been before—setting things up for Angular.dart dependency injection in test, injecting my application controller and backend service, along with a mock HTTP service and test bed.

As for the the test itself, I can schedule asynchronous tasks, which will be executed in serial, one-per “schedule.” This ends up looking like:
    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);

        schedule(()=> http.flush());
        schedule(()=> tb.rootScope.$digest());
        schedule((){
          expect(
            tb.rootElement.query('ul').text,
            contains('Test Appt #1')
          );
        });
      })
    );
That… is very pretty.

The http.whenGET() and tb.compile() could just as easily move into another setUp(). Even here, they are so compact that they hardly seem out of place.

After the setup, I schedule the http.flush() and scope.$digest() that I determined were necessary Angular.dart things under test. The last schedule is to set my expectation. Compact. Easy to read. Full browser stack, acceptance test. I like it!


Day #921

No comments:

Post a Comment