Monday, October 28, 2013

Proper module() and inject() Testing in Angular.dart


Some nights can be summed up as: why-wont-it-work, why-wont-it-work, why-wont-it-work, aw-ferget-it… oooh-it-works. Last night's Angular.dart testing was one of those messes that ended the night working, I think. Tonight, I make sure that I have figured out the proper way of testing the Dart port of AngularJS.

With a day to reflect on last night's madness, I realize that understanding Angular.dart and testing Angular.dart means that I need a better handle on the DI dependency injection framework. Specifically, the two most important methods are type(), which injects an object of the specified type into the module, and value(), which injects an already defined object into the module.

Armed with those two bits of trivial, I revisit my test setUp() from yesterday:
  group('Appointment Backend', (){
    var http_backend;
    setUp(() {
      setUpInjector();
      module((Module module) {
        http_backend = new MockHttpBackend();
        module
          ..value(HttpBackend, http_backend)
          ..type(AppointmentBackend);
      });
    });
    // Tests go here...
  });
The setUpInjector() function needs to be called before each test, which is what the setUp() block does in Dart unittest. As the name suggests, setUpInjector() establishes a mock Angular module with support for injecting things under test with the help of two other helper methods: module() and inject().

The inject() and module() helpers are very similar. The stated difference between the two is that module() is for injecting types whereas inject() is for getting an injector back so that tests can operate on an injected instance.

At first, the distinction seems somewhat subtle. Inside the module() call, I create an instance of MockHttpBackend(), which I then inject into the mock angular module with the value() method, which injects instances rather than types:
      module((Module module) {
        http_backend = new MockHttpBackend();
        module
          ..value(HttpBackend, http_backend)
          ..type(AppointmentBackend);
      });
I am injecting an instance (http_backend) and a type (AppointmentBackend, the module actually being tested) thanks to DI's value() and type(). So the distinction between module() and inject() is not that instances of objects can be used, it is how they are used.

The module() function parallels the normal DI module() function, building up a series of modules and types are injected. The inject() method then injects one last module to grab instances of injected classes for testing. The mock Angular module has an instance of my AppointmentBackend—to test it, I inject one last object to retrieve that instance for testing:
    test('add will POST for persistence', (){
      inject((AppointmentBackend server) {
        http_backend.
          expectPOST('/appointments', '{"foo":42}').
          respond('{"id:"1", "foo":42}');

        server.add({'foo': 42});
      });
    });
In that sense, the anonymous Dart function that is being injected behaves like a DI class—the function is in turn injected with the types specified in the argument list.

Now that I think about it, there is no need for me to muck with the instance of MockHttpBackend in the setUp() block. I can inject it into the module by type and then retrieve it in the inject() function:
  group('Appointment Backend', (){
    setUp(() {
      setUpInjector();
      module((Module _) => _
        ..type(MockHttpBackend)
        ..type(AppointmentBackend)
      );
    });

    test('add will POST for persistence', (){
      inject((AppointmentBackend server, HttpBackend http) {
        http.
          expectPOST('/appointments', '{"foo":42}').
          respond('{"id:"1", "foo":42}');

        server.add({'foo': 42});
      });
    });
That is a nice, concise test. Given a module with an Http service and an AppointmentBackend, I expect the described POST if I add a record. It is not quite end-to-end, but it is very, very powerful.

Speaking of non-end-to-end tests, I have another pre-exiting test that sets expectations on the interaction between the AppointmentController and the AppointmentBackend (the latter of which is tested above against Http). Since these are unit tests, I wanted to verify that an action that triggers an add() in the UI controller triggered an add() in the backend service. For that, I injected a mock backend that could keep track of the number of times a particular method was called.

This is a good use case for value() in an Angular.dart setUp() block:
  group('Appointment controller', (){
    setUp((){
      setUpInjector();
      var server = new AppointmentBackendMock();
      module((Module _) => _
        ..value(AppointmentBackend, server)
        ..type(AppointmentController)
      );
    });
    // Tests here...
  });
I cannot just inject the AppointmentBackMock because the class definition is too spartan:
class AppointmentBackendMock extends Mock implements AppointmentBackend {}
But I can use value() to tell my test module that it should be injected as an AppointmentBackend.

The test can then inject() the mock server and the controller so that is can invoke the add() method on the controller and check the expectation that the method of the same name was called on the mock server:
  group('Appointment controller', (){
    setUp((){
      setUpInjector();
      var server = new AppointmentBackendMock();
      module((Module _) => _
        ..value(AppointmentBackend, server)
        ..type(AppointmentController)
      );
    });
    tearDown(tearDownInjector);

    test('adding records to server', (){
      inject((AppointmentController controller, AppointmentBackend server) {
        controller.newAppointmentText = '00:00 Test!';
        controller.add();

        server.
          getLogs(callsTo('add', {'time': '00:00', 'title': 'Test!'})).
          verify(happenedOnce);
      });
    });
  });
After rewriting a few more tests, I have the entire test suite passing again—now with a more Angular feel and usage. I still believe that opportunities exist for getting closer to end-to-end acceptance testing with this approach. I will cover that tomorrow. But even this minimal interaction testing is concise-yet-powerful. I am already very excited to start using it more.


Day #918

No comments:

Post a Comment