Rewriting Angular Controller Using ES6 Syntax

Migrating existing angular controller from ES5 to ES6 is relatively easy.
Here is an example of angular controller written in ES5.

angular.module('corporateChallengeApp')
       .controller('DashboardCtrl', DashboardCtrl);
    DashboardCtrl.$inject = [
         'sportService',
         'Auth',
         'sportUtils'];
    function DashboardCtrl(sportService, Auth, sportUtils) {

        var vm = this;
        vm.sports = [];
        vm.mySports = [];
        vm.initialized = false;
        vm.currentUser = Auth.getCurrentUser();

        init();

        function init() {
            getSports();
        }

        function getSports() {
            var year = new Date().getFullYear();
            return sportService.getSports(year)
                .then(function (data) {
                    vm.sports = data;
                    vm.mySports = sportUtils.getSportsByUser(vm.sports,vm.currentUser);
                    vm.initialized = true;
                    return vm.sports;
                });
        }

    }

Now let’s write the above controller using ES6 Syntax

    class DashboardCtrl {
        /*@ngInject*/
        constructor(sportService, Auth, sportUtils) {
            this.sportService = sportService;
            this.Auth = Auth;
            this.sportUtils = sportUtils;
            this.sports = [];
            this.mySports = [];
            this.initialized = false;
            this.currentUser = Auth.getCurrentUser();
            this.getSports();
        }

        getSports() {
            var year = new Date().getFullYear();
            return this.sportService.getSports(year)
                .then(data => {
                    this.sports = data;
                    this.mySports = this.sportUtils.getSportsByUser(this.sports, this.currentUser);
                    this.initialized = true;
                    return this.sports;
                });
        }
    }
    angular.module('corporateChallengeApp')
           .controller('DashboardCtrl', DashboardCtrl);

Now you’ll need to use transpiler like Babel to transpile it down to ES5 when you build your project.
This only uses the class and arrow function feature, there are many other features in ES6.

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!

How to Unit Test an Angular Application – Service with Requests to Backend

I noticed that a lot of developers write unit tests that is not really unit testing the code. Many dependencies are not mocked correctly or not mocked at all so unit test is not serving its true purpose.

Personally, I was struggling with how to do unit test quite a bit just like many others but now I think I have a solid concept on how to write unit test correctly.

There will be more posts coming up covering unit testing different part of your Angular application.

For now, let’s start with a simple service with backend calls.

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

Consider the above service, what do we want to unit test for in this case?
Only thing we really care about is that the functions are making the correct HTTP requests to the server. As long as the URL is correct, we really don’t care for anything else on the unit test. Your service code has no control of what server will return to you. Now let’s see how the unit test would look like.

describe('my service', function () {
  var MyService, httpBackend;

  beforeEach(module('myApp'));

  beforeEach(inject(function (_MyService_, $httpBackend) {
    MyService = _MyService_;
    httpBackend = $httpBackend;
  }));

  it('should match URL when get users', function () {
    httpBackend.expectGET('/users');
    MyService.getUsers();
  };

  it('should match URL when get user by ID', function() {
    httpBackend.expectGET('/users/1');
    MyService.getUserById(1);
  };
});

As far as unit test this service goes, this should be all you need. You don’t care about the data that is returned to you because provided the input is correct (the endpoint URL), the result testing should be covered in the unit test for the server side.