You’re probably thinking to yourself…

Unit testing. This has been done to death.

Yeah - it has. However, Angular testing is a bit of an interesting one. It’s probably one of the few things that I don’t like the documentation for in the Angular docs for it. It’s too brief and absolutely doesn’t cover all of the basic use cases that a developer (new or seasoned) would very quickly encounter.

I think most of this is to blame (I’m not using the word ‘blame’ in a negative sense here) on the fact that popular testing frameworks are agnostic/unaware of Angular itself - therefore their documentation doesn’t mention Angular at all. This, combined with the Angular documentation covering unit testing being reasonably short/incomplete (in my opinion) makes for a difficult time for most developers who haven’t had to deal with this sort of thing before.

I just wanted to demonstrate what I mean, and provide an example of what you might do when you encounter something like this.

The situation

For this example, let’s assume we have a controller, called TestController that looks like this:

(function() {
  angular
    .module('test')
    .controller('TestController', TestController);

  /* @ngInject */
  function TestController($scope, testService) {
    /**
     * Sets up our controller for use.
     */
    $scope.activate = function() {
      $scope.users = [];
    }
    
    /**
     * Gets a list of all users.
     */
    $scope.getUsers = function() {
      testService.getUsers()
        .then(function(response) {
          $scope.users = response.data;
        });
    }

    $scope.activate();
  }
})();

And our service, testService that is injected into TestController looks like this:

(function() {
  angular
    .module('test')
    .factory('testService', testService);

  /* @ngInject */
  function testService($http) {
    var service = {
      getUsers: getUsers
    };

    return service;
    
    function getUsers() {
      return $http.get('/users');
    }
  }
})();

Let’s also make an assumption that the GET /users endpoint returns an array of user objects that look like this:

{
  id: Number,
  name: String
}

We want to test TestController.

We definitely want to test the following:

  • That our controller has the apporiate default values when the controller is instantiated.
  • That getUsers() is able to call a service which returns a promise, that when resolved will respond with a list of users. $scope.users will be populated after this is complete.

We definitely do not want the following:

  • To hit our API server at all.
  • To write any convoluted code. This is a simple setup and we shouldn’t have to get too fancy.

For the sake of this article, we can assume that the testing framework being used is Jasmine. We are also using Angular Mocks

The most important thing to keep in mind here is that since testService.getUsers() returns a promise, we ideally want to be dealing with promises in our tests (rather than mocking them our completely).

So let’s look at what we can do:

describe('TestController', function() {
  // Some variables that we will always need access to.
  var $scope,
      deferred,
      TestController;

  // Some test users. This is what our API response will look like.
  var testUsers = [
    { id: 1, name: 'Dave' },
    { id: 2, name: 'Alice' },
    { id: 3, name: 'Bob' }
  ];

  // Executed before each test suite.
  beforeEach(function() {
    // Declare our module. This will give us access to dependencies 
    module('test');

    // Injected into each test suite.
    inject(function($rootScope, $controller, _$q_, _testService_) {
      // Declare a new scope.
      $scope = $rootScope.$new();
      
      // Create a promise object.
      deferred = _$q_.defer();

      // Set up a spy to return our promise when getUsers() is called on our service.
      spyOn(_testService_, 'getUsers').and.returnValue(deferred.promise);

      // Instantiate a new instance of TestController.
      TestController = $controller('TestController', {
        $scope: $scope,
        testService: _testService_
      });
    });
  });

  it('Should exist', function() {
    expect(TestController).toBeDefined();
  });

  it('Should have the appropriate default values', function() {
    expect($scope.users).toEqual([]);  
  });

  it('Should be able to retrieve a list of users when requested.', function() {
    // Call our getUsers() function which will result in a promise waiting to be resolved.
    $scope.getUsers();

    // Resolve our promise and return our test data.
    deferred.resolve(testUsers);

    // Important: we need to call $scope.$apply() so that the digest cycle is triggered and any outstanding promises are resolved/rejected.
    $scope.$apply();

    // At this point, we can make our assertions.
    expect($scope.users).toBeDefined();
    expect($scope.users).toEqual(testUsers);
  });
});

There we go! We have a pretty nice way of testing a controller that has a service injected into it that returns a promise. Please keep in mind, this is just my preferred way of testing these kind of things and if you have any insights as to why this could be not as good as another way of doing things, I’d love to hear about it (either comment on the post or hit me up via email).

-Dave