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: anyThen, 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