Unit Test a Controller in Angularjs

This post will go over how to unit test a controller in angular.
Controller and service design assumptions:

  1. Controller does not make API call to the backend directly.
  2. API call to the backend is always going through a service which returns a promise.

Considering the following service and controller

//MyService
angular.module('myApp').service('MyService', MyService);
MyService.$inject = ['$resource'];
function MyService($resource) {
  var myResource = $resource('/users/:id', {}, {});
  return {
    getUsers: function() {
      return myResource.query().$promise;
    },
    getUserById: function() {
      return myResource.get({id: userId}).$promise;
    }
}
//MyController
angular.module('myApp')
       .controller('MyController', MyController);
MyController.$inject = ['MyService', 'ThirdPartyService'];

function MyController(MyService, ThirdPartyService) {
  var _this = this;
  _this.MyService = MyService;
  _this.ThirdPartyService = ThirdPartyService;
  MyService.getUsers().then(function (data) {
    _this.users = data;
  }, function () {
    _this.error = true;
  });

  _this.processUsers = function (users) {
    var processedUsers = 
      ThirdPartyService.doSomething(users);
    return processedUsers;
  };

  _this.doMoreUserProcessing = function (users) {
    var processedUsers = 
      ThirdPartyService.doMoreStuff(users);
    return processedUsers;
  };

  _this.getSpecificUser = function (id) {
    MyService.getUserById(id).then(function(data) {
      return data;
    }, function() {
      //Some error handling
    });
  };
}
//My Unit Test
describe('MyController', function() {
    var $scope;
    var rootScope;
    var controller;

    //Mocking services
    var MyServiceMock = jasmine.createSpyObj('MyService',
      ['getUsers', 'getUserById']);

    /*
      Mocking third party services is not necessary.
      Assume they are well tested by the developers.
      However, you can mock them if you choose to.
    */
    var ThirdPartyServiceMock = 
      jasmine.createSpyObj('ThirdPartyService',
        ['doSomething', 'doMoreStuff']);
    beforeEach(module('myApp'));

    /*
      Create different describe blocks to
      mock the success server return vs error server return
    */
    describe('Success callback test cases', function () {
        beforeEach(
          inject(function ($rootScope, $controller, $q)    {
            $scope = $rootScope.$new();
           
            //Mocking a promise return by using $q.when
            MyServiceMock.getUsers.andReturn($q.when([
              {username:'user1', id: 1},
              {username:'user2', id: 2},
              {username:'user3', id: 3}
            ));
            MyServiceMock.getUserById.andReturn($q.when(
              {username: 'user1', id:1}
            ));

            controller = $controller('MyController', {
                ThirdPartyService: ThirdPartyServiceMock,
                MyService: MyServiceMock
            });
        }));

        it('should initialize users', function () {
            expect(controller.users).not.toBeDefined();
            /*
               Run a digest cycle after controller runs to
               execute the "then()" blocks
            */
            $scope.$digest();
            expect(controller.users.length).toBe(3);
        });
        it('should test user processing', function () {
            var users = [{'username': 'test', id: 1}];
            controller.processUsers(users);
            /*
              Only need to test if the function is called.
              You can use toHaveBeenCalledWith() if you want
              your test to be robust.
              Since ThirdPartyService is already created
              as spy object in beforeEach block,
              no need to create another "spyOn" object here.
            */
            expect(
              controller.ThirdPartyService.doSomething
            ).toHaveBeenCalled();
            
            controller.doMoreUserProcessing(users);
            expect(
              controller.ThirdPartyService.doMoreStuff
            ).toHaveBeenCalled();
        });

        it('should test get specific user', function () {
            var user = controller.getSpecificUser(1);
            $scope.$digest();
            expect(user.username).toBe('user1');
        });
    });

    describe('Error callback test cases', function () {
        beforeEach(
          inject(function ($rootScope, $controller, $q)    {
            $scope = $rootScope.$new();
            rootScope = $rootScope;
           
            /*
                Mocking a promise error return by 
                using $q.reject. This simulates all
                4XX or 5XX returns on server call.
            */
            MyServiceMock.getUsers.andReturn(
            $q.reject());
            MyServiceMock.getUserById.andReturn(
            $q.reject());

            controller = $controller('MyController', {
                ThirdPartyService: ThirdPartyServiceMock,
                MyService: MyServiceMock
            });
        }));

        it('should initialize with error', function () {
            expect(controller.users).not.toBeDefined();
            expect(controller.error).toBeFalsy();
            /*
               Run a digest cycle after controller runs to
               execute the "then()" blocks
            */
            $scope.$digest();
            expect(controller.users).not.toBeDefined(); 
            expect(controller.error).toBeTruthy();
        });

    });
});

Unit test the service in this example

Feel free to provide any feedback!