Unit Testing AngularJS Controllers, Views, and More: Part Two

Link to Part One of this Series on Angular Unit Testing using Duck-Angular

When we last left off, I’d discussed the structure of the application, and why I was using RequireJS (and Q). In this post, I’ll speak of the way the tests are set up. For the most part, it will parallel the application folder structure, but there are a few things that will be worth noting. For reference, I’m still using the AngularJS-RequireJS-Seed project to talk you through the details. The Duck-Angular project is here.

The files pertinent to the initial part of this post are:

  • test-main.html
  • test-main.js
  • test-setup.js
  • test.config.js

It might be easiest to explain these files by drawing parallels between them and the production bootstrap code.

Unit Test Setup

  • test-main.html is analogous to index.html. This is the file that kicks everything off. This may be run inside an actual browser like Chrome, or it may be run from the command line using libraries like Mocha-PhantomJS. Perusing this file indicates that it loads up test-main.js.
  • test-main.js does not have any analog in the production code because it does a bunch of test-specific things. Among them:
    • It lists the unit test files which will be run.
    • It sets up the parameters for Mocha, like timeout and the assertion style. It also enables the Mocha-as-Promised extension.
    • It sets up Chai’s assertion style (“bdd” in this case), and also enables Chai-as-Promised.
    • Finally, it runs the unit tests.
    • As part of test-main.js, the file test-setup.js is also loaded. test-setup.js is simply the combined unit test analog of bootstrap.js and app.js, but crucially, even though it has all these functions to setup/bootstrap the app, it does not actually execute them. Think of these functions as helper functions, which can be used by the unit test to set up the AngularJS environment. The important thing to note here is, if you’re using Duck-Angular, you’ll never need to all these methods directly. In this example, I’m using spec-helper.js a lot, and that’s the module which actually uses these bootstrapping functions defined in test-setup.js.
  • Finally, test.config.js maps the test-specific modules to the correct file paths.

The First Controller unit test

We are (mostly) set to write our first unit test against a controller. This one will probably be a little anticlimactic, because at the end of all this setup, all you will really do is load a RequireJS module, and use the resulting object in your test. But this does illustrate how easily you can test AngularJS objects if you don’t start them off…as AngularJS objects. But fret not, we’ll be writing more involved tests (views and controllers and services and whatnot) very soon 🙂

Let’s look at the controller that we want to unit test. This is from controller2.js

define([], function() {
  return function ($scope, $location, $q, service2) {
    var deferredLoaded = $q.defer();
    this.loaded = deferredLoaded.promise;
    $scope.go = function() {
        console.log("Was called");
        $location.path("/route1");
    };

    console.log("In controller2");
    service2.get().then(function(data) {
        $scope.data = data;
        console.log("Service2 returned");
        deferredLoaded.resolve();
    });
  };
});

The controller defines a method go() on its scope, which navigates to /route1. The other thing it does, on initialisation, is call service2.get(), presumably to get some data, which it then sets back on to the scope. This is an asynchronous operation. Having done this, it fulfills a promise which is available to anyone who might want to act on it. The variable ‘deferredLoaded’ is used for this purpose.

The Loaded Promise Convention for Controllers

In most cases, it makes sense to have the controller fulfill a promise to signal that it is done “loading”. Loading implies completing any and all asynchronous operations. This is very useful for predictable unit tests, because if there’s a promise that has not been fulfilled in time, and an assertion depends upon that promise having been fulfilled, you have unpredictable tests which sometimes pass, and sometimes fail. Duck-Angular expects your controller to have a loaded promise. Even if your controller does not perform any asynchronous operation, having a pre-resolved promise is simple boilerplate that can be abstracted away.

The first test that we’ll write simply asserts that the scope is set up as intended. We’ll mock out service2 so that we are only testing controller behaviour. Here is a snippet from controller2-test.js.

it("can set data on scope", function () {
  var service2 = { get: sinon.stub() };
  service2.get.returns(Q("Some Mock Data"));
  var location = { path: sinon.stub()};
  var scope = {};
  var controller = new Controller2Ctor(scope, location, Q, service2);
  return controller.loaded.then(function() {
    expect(scope.data).to.eql("Some Mock Data");
  });
});

So, let’s dissect the dependencies.

  • service2 has been stubbed to return “Some Mock Data“. Note that it returns a promise, instead of the value directly. This maintains the illusion that the client of service2 is still dealing with asynchronous code.
  • scope is just an empty object, no surprises here.
  • location is an object with a single stub method get(). Strictly speaking, this is not needed here; an empty object would have sufficed.
  • The $q dependency has been satisfied with Q. There is a reason why AngularJS’ $q library will not work (in many cases) inside unit tests. I’ll write a simple test to demonstrate this later, and explain why.

Anyway, the rest of the code is more or less easy to follow. We make an assertion about the state of scope after the controller.loaded promise is fulfilled, because, of course, that’s when we can be sure that the state of the scope has been altered. The other interesting thing to note is that we are actually returning a promise from the test. If we were using the raw Mocha library, we’d have to invoke done() to signal to Mocha that the test has completed. However, because we are using Mocha-as-Promised, we return a promise at the end of a test. Whether the promise is fulfilled or rejected, determines the success (or failure) of the unit test.

Sinon to the Fore

On without pause to the next controller test. This one will be similar in structure to the previous test, but will assert that triggering the go() method in the controller causes a navigation to /route1. Of course, in this restricted AngularJS-free sandbox, we won’t really perform this navigation; we will simply verify that $location.path() method is called with the appropriate parameter (“/route1” in this case).

Let’s look at the test. This is from controller2-test.js.

it("can navigate to another route", function () {
  var service2 = { get: sinon.stub() };
  service2.get.returns(Q("Some Mock Data"));
  var location = { path: sinon.stub()};
  var scope = {};
  var controller = new Controller2Ctor(scope, location, Q, service2);
  return controller.loaded.then(function() {
    scope.go();
    expect(location.path.calledWith("/route1")).to.be.ok;
  });
});

Mostly the same, except the part inside the fulfillment handler. The scope.go() method is fired, and we expect location.path() to be called with “/route1“. This kind of test is very useful when firming up logic inside controllers.

Inject what you Want, let AngularJS do the Rest

Many times, you may want to mock out only some of the dependencies, but need the rest to be fulfilled by the production dependencies. To demonstrate this, let’s look at controller1.js.

    define([], function() {
      return function ($scope, service1, $q) {
        var deferredLoaded = $q.defer();
        this.loaded = deferredLoaded.promise;

        console.log("In controller1");
        service1.get().then(function(data) {
            $scope.x = data;
            deferredLoaded.resolve();
        });
      };
    });

This is very similar to controller2.js, and simpler. All it does is get some data from service1, and set it on the $scope as x. Let’s assume that we want to unit test this controller, but we do not want to mock out service1‘s dependency. How do we go about doing this? Here’s the short answer, from controller1-test.js.

    define(["spec_helper", "Q"], function (mother, Q) {
      describe("Controller1 Test", function () {
        it("sets up real registered service if dependency is not explicitly provided", function () {
          var testScopeMock = {};
          return mother.createController("route1Controller", { $scope: testScopeMock, $q: Q}).then(function(controller) {
            expect(testScopeMock.x).to.eql("Real Service1 Data");
          });
        });
      });
    });

If you look at service1.js, it returns a promise with the value “Real Service1 Data“, and that is the expected value we are testing against. In the test above, we inject $scope and $q, but we do not inject the service1 dependency. Yet, that dependency is fulfilled. What sort of magic is happening inside mother.createController()?

A Gander inside Duck-Angular

As it turns out, not a lot. We’ll have more than one occasion to examine the guts of Duck-Angular, and this is one of them. Before I dive into the code inside mother.createController(), it will be instructive to review how you can retrieve a controller which has been registered with AngularJS.

The easiest way is through the $controller service. The $controller service itself may be accessed through the application’s injector. After that it is mostly a matter of asking for the controller by its registered name. However, to get the injector, we need to have bootstrapped the application. This is precisely what happens inside the createController() function. Here’s the relevant code from spec-helper.js.

mother.createController = function createController(controllerName, dependencies) {
  return initApp().spread(function (injector, app) {
    mother.injector = injector;
    var container = new Container(injector, app);
    var resourceBundleFactory = container.injector.get("ngI18nResourceBundle");

    return resourceBundleFactory.get("en").then(function (resourceBundle) {
      var controller = container.controller(controllerName, dependencies);
      dependencies.$scope.resourceBundle = resourceBundle.data;
      mother.resourceBundle = resourceBundle.data;
      return controller;
    });
  });
};

You can safely ignore all the resourceBundle calls. For now, suffice it to say that the initApp() function is called (remember, initApp() was defined in test-setup.js). After this, the Container is set up with the injector and the app. When the time comes to get the controller, we simply ask the Container to give it to us. This is the relevant code from duck-angular.js.

self.controllerProvider = self.injector.get("$controller");
....
this.controller = function (controllerName, dependencies) {
  var deferred = Q.defer();
  var controller = self.controllerProvider(controllerName, dependencies);
  controller.loaded.then(function () {
    deferred.resolve(controller);
  });
  return deferred.promise;
};

It basically translates to a simple call the the $controller service. The $controller service is responsible for accommodating our injected dependencies, as well as autowiring those which have not been explicitly injected.

In the next part of this series, I’ll talk of the nuances of using Q instead of $q, a nice way to mock $http‘s success()/failure() clauses, and start wiring up views to controllers in our unit tests, while digging more inside Duck-Angular’s internals.