Testing AngularJS Applications

This blog post helps to finetune development process. Writing about it, will make me think twice and make some improvements along the way.

  1. Introduction
  2. Test environment setup
  3. Controller
  4. Service
  5. Directive
  6. Filters
  7. Events
  8. Watches
  9. Promise
  10. HTTP Requests
  11. WebSockets
  12. Timeout, Debounce and setInterval
  13. Templates+UI
  14. Refactoring
    • Data Model Architecture
    • Better Controllers
    • Reveal Module Pattern
    • Analytics
    • Custom Matchers
  15. References

Introduction


This ship has been sailing for years and has never sunk - let's throw away the lifeboats! - peterclary

Have you ever googled "Angularjs Unit testing", "Testing AngularJS Directive", "Testing AngularJS Promise or Callbacks or Events", these series are just for you! In these blog post series, I highlight recipes that helped me for over a year in making hoo.gy. I will emphasize on small, working Unit testing recipes, that you can start using in your own test cases. I compiled these recipes to help me with future projects, and I will happy if It helps someone else.

Format: Each case, will have 3 components. Code, Test and References. Exceptions may apply, if some nuances are required to make the code more understandable.

Some knowledge came from a course I took at Concordia University(Advanced Programming Practices), experience I gained while refactoring my side project(hoo.gy) and day job projects, as well as incredible online ressources(I will give credit, and links along the way).

[Just] Say no to e2e testing, and Unit Testing Angular Views To Avoid the Integrated Test Scam


Setup

Code structure

  • Structure behaviours from specifications
  • Import AngularJS
  • Import libraries

Controller

Unit test AngularJS Controllers

Code

  • Code for a controller is straight forward.
  • It initializes models for the view.
  • Controller delegates actions to other layers of the application. Data layer, Service or other libraries.
angular  
  .app('hoogy')
  .controller('MainController', MainController);
MainController.$inject = ['$timeout'];

function MainController($timeout) {  
  var vm = this;
  vm.user = {};//model

  vm.initMainController() = function() {
      //model initialization
      vm.user.name = 'Mr. Clarck';
      vm.user.job = 'CTO @hoopeez';
    };

  //for scope reasons
  return vm;
}

Unit Testing

  • Testing a controller is straight forward as well.
  • In test case, module hoogy loads application and components associated to it.
  • Hint, angular.module('hoogy') is loaded in beforeEach(module('hoogy')).
describe('MainController', function() {  
  var $scope;
  var $controller;

  beforeEach(module('hoogy'));
  describe('Initialization', function() {
    beforeEach(inject(function($rootScope, $injector) {
      $scope = $rootScope.$new();
      $controller = $injector.get('$controller');
    });

    it('should initialize user model', function() {
      // Assert
      assert.isTrue(vm.alertVisible);

      //first way
      var controller = $controller("MainController", {$scope: $scope});
      controller.initMainController();//initialization
      expect(controller.user.name).toBe('Mr. Clarck');

      //second way   
      controller = $controller("MainController as vm", {$scope: $scope});
      $scope.vm.initMainController();//initialization
      expect($scope.vm.user.name).toBe('Mr. Clarck');
      expect(controller.user.name).toBe('Mr. Clarck');

        });
  });
});

Reference


Service

Unit test AngularJS Services

  • In most of cases, Controller delegates processing tasks to Service layer
  • At least, two set of services are used within your application.
  • First Service category communicates with your backend, e.g HoogyService or StripeService.
  • Second Service category Service is used for data processing.

Code

angular  
  .module('hoogy')
  .factory('HoogyService', HoogyService)
  .factory('StripeService', StripeService);

  //Data Service
  HoogyService.$inject = ['$log'];
  function HoogyService() {

  }

  //Payment wrapper
  StripeService.$inject = ['$log'];
  function StripeService() {

  }

Unit Testing

  • Each Service has its own test case
  • For simplicity, two services are sharing same test case.
describe('Services', function() {

  beforeEach(module('hoogy'));
  var $httpBackend;
  var HoogyService;
  var StripeService;
  var deferred;

  beforeEach(inject(function($injector, $q) {
    deferred = $q.defer(); //used with promises
    HoogyService = $injector.get('HoogyService');
    StripeService = $injector.get('StripeService');
    $httpBackend = $injector.get('$httpBackend');
  }));

  it('should make async tasks', function() {});

  //afterEach finishes and clean existing httpBackend
  afterEach(function() {
    $httpBackend.verifyNoOutstandingExpectation();
    $httpBackend.verifyNoOutstandingRequest();
  });
});

Reference


Directive

Unit test AngularJS Directives

  • Testing directive is tricky.
  • Directives have Controllers, Other directives, Filters ... it is really a mess :-)
  • Directives combine both HTML elements(view), and controller actions(events).
  • Hint: You don't need to test Views in Unit testing, this is for E2E testing.

Code

angular  
  .module('hoogy')
  .directive('searchDirective', SearchDirective);

//Directive Code 
SearchDirective.$inject = ['$scope'];  
function SearchDirective($scope) {  
  return {
    restrict: 'EAC',
    scope: {},
    controller: SearchController,
    controllerAs: 'vm',
    bindToController: true,
    template: '<div>{{isInitialized}}</div>'
  };
}

//Directive Controller 
SearchController.$inject = ['$scope'];  
function SearchController($scope) {  
  var vm = this;
  vm.isInitialized = true;
}

Testing

  • Trick#1 Working with HTML templates, call $httpBackend.flush() before $rootScope.$digest()
  • Trick#2 Template has to be available before digest
  • Trick#3 Pick preference between $scope.$digest() and $rootScope.$digest() for consistency and clarity across codebase.
describe("Search Directive", function() {  
  var element;
  var scope;
  var $scope;
  var controller;
  beforeEach(module("hoogy"));
  //include template if you have minified templates
  //beforeEach(module("template"));

  beforeEach inject(function($compile, $rootScope) {
    $scope = $rootScope.$new();

    //Instantiate directive.
    //Gotcha: Controller and link functions will execute.
    element = angular.element("<search></search>")
    $compile(element)($scope);
    $scope.$digest();

    //Grab controller instance
    controller = element.controller();

    //scope isolation
    //Grab scope. Depends on type of scope.
    //See angular.element documentation.
    scope = element.isolateScope() || element.scope();
    //$scope will still be accessible and has all variables
  })

  it("should do something to the scope", function() {
    expect(scope.isInitialized).toBeDefined();
  });
});

Gotchas

Testing nested directives is not an easy task. I found some ressources that may shade a light to that:

References


Filters

Code

angular  
  .module('hoogy')
  .filter('capitalize', [function() {
    'use strict';
    return function(input, scope) {
      return input.substring(0, 1).toUpperCase() + input.substring(1);
    };
  }]);

Unit testing

describe('Filter', function() {  
  var $filter;
  module.beforeEach(module('hoogy'));
  beforeEach(function() {
    inject(function($injector) {
      $filter = $injector.get('$filter');
    });
  });

  it('should transform parameters', function() {
    var str = 'hello world';
    var capitalized = 'Hello World';
    expect($filter('capitalize')(str)).toBe(capitalized);
  });
});

References


Events

Code

  • Example for $emit, $broadcast and $watch events
  • Events triggered by HTML components are not in scope of this blog post.
  angular
    .app('hoogy')
    .controller('MainController', MainController);

  MainController.$inject = ['$timeout', '$scope'];
  function MainController($timeout, $scope) {
    var vm = this;
    //model
    vm.user = {}; 

    vm.initMainController() = function() { /**...*/ };
    $scope.$watch('vm.user', function(user) {
      vm.user.name = 'Mr. '+ user.name;//@warn: may not work
      $rootScope.$broadcast('user::name::changed');
    });
    //make this public
    return vm;
  }

Testing

describe("MainController", function() {  
  //initializations
  var $scope;
  var $rootScope;
  var $controller;
  var MainController;

  beforeEach(inject(function($injector) {
    $rootScope = $injector.get('$rootScope');
    $controller = $injector.get('$controller');
    spyOn($rootScope, '$broadcast');
    spyOn($rootScope, '$broadcast').andCallThrough();
    spyOn($scope, '$emit');
    MainController = $controller('MainController', {
      $scope: $scope
    });
    //hack: Not always required
    $scope.vm = MainController;
  }));

  describe("#$watch expression", function() {
    it("should broadcast name change", function() {
      // make an initial selection
      MainController.user.name = 'Henry Ford';
      //run code to test $emit
      $scope.$digest();
      expect($rootScope.$broadcast).toHaveBeenCalledWith('user::name::changed');
      expect(MainController.user.name).toEqual('Mr. Henry Ford');
    });
  });
});

Reference


Watches

Code

Following example is for testing a $scope.$watch callback.
$rootScope.$watch callback can be tested the same way.
Testing watches this way, applies to Controller watches, Directive controller watches.

angular  
  .module('hoogy')
  .controller('MainController', function() {

    var vm = this;
    var selection = null;
    vm.hello = 'Hello, world!';
    vm.current = null;

    $scope.$watch('vm.hello', function(current, previous) {
      selection = previous;
    });

    vm.change = function(revert) {
      if (revert) {
        vm.current = selection;
      }
    };
  });

Testing

$scope.$digest() call makes $watch be aware of changes in $scope.
It triggers callback execution down the road.

describe('MainController', function() {  
 var $scope;
 var controller;

//loading hoogy application
 beforeEach(module('hoogy'));
 describe('#watch ::vm.hello', function() {
   beforeEach(inject(function($rootScope, $controller) {
     $scope = $rootScope.$new();
     // scope attached methods and variables are copied into $scope
     controller = $controller('MainController', { $scope: $scope});
     //hack: Not always necessary
     $scope.vm = controller;
   }));

    it('should detect change', function() {
         //initial selection
         controller.selection = 'Hi';
         $scope.$digest();

        //another change
         controller.selection = 'Bye';
         $scope.$digest();
         //revert to previous
         controller.change(true);
         expect(controller.selection).toBe('Hi');
         });
     });
 });

References


Promise

    #helps to resolve pending promises [reference](http://stackoverflow.com/a/24021094/132610)
    $rootScope.$digest();

Please visit these links provided in references.

Reference


Websocket

Testing

  • To spy on existing send function(WebSocket's .protype.send), prototype has to be included.
  • On Send event, response is returned right away.
describe('WebSocket Wrapper/Service', function() {  
  'use strict';

  //native objects holder
  var RealWindow;
  var RealWebSocket;

  beforeEach(inject(function($injector) {
    //Mocking WebSocket and WebSocketService
    $window = $injector.get("$window");
    RealWindow = window;
    RealWebSocket = WebSocket;

    //Replace native WebSocket
    window = $window;
    WebSocket = $window.WebSocket || window.WebSocket;

    //New WebSocket
    var WebSocketSpy = jasmine.createSpy();
    WebSocketSpy.onmessage = function(data) {
      return data;
    };
    WebSocketSpy.send = function(data) {
      return data;
    };

    //spying on WebSocket.send method - to echo the response
    spyOn(WebSocket.prototype, "send").and.callFake(function(outgoing) {
      this.onmessage(outgoing); //bounce back the message
    });
    //replace WebSocket with Stubbed WebSocket
    spyOn(window, 'WebSocket').and.callFake(function() {
      return WebSocketSpy;
    });
  }));

  afterEach(function() {
    //re-assign Real native objects
    window = RealWindow;
    WebSocket = RealWebSocket;
  });

});

Reference


Timeout

Objective of this post is to shade a light on testing timely, repetitive functions.
By timely, repetitive functions, I refer to use of timout/interval/debounce and throthle to perform actions repetitive or future actions.

Code

angular  
  .module('hoogy')
  .controller('MainController', MainController);
MainController.$inject = ['$timeout', '$scope'];

function MainController($scope, $timeout) {  
  var vm = this;
  vm.name = 'Def';
  vm.interval = function() {
    setInterval(function() {
      vm.name = "Intel";
    }, 500);
  };
  vm.timedout = function() {
    $timeout(function() {
      vm.name = "Timer";
    }, 500)
  };
  vm.debounced = function() {
    _.debounce(function() {
      vm.name = "Bouncer";
    }, 500)
  };
  vm.throttled = function() {
    _.throttle(function() {
      vm.name = "Throttler";
    }, 500)
  };
}

Testing

Clock in Jasmine works while testing setInterval() and setTimeout(). In fact, Jasmine mocks those two specific functions to run synchronously. "...there's a pull request for Jasmine to mock the Date object, which would allow for testing functions like _.debounce() without mocking it ..."

To test _.debounce() you'll have to mock it to run synchronously, preferably as a spy or you can just override the function. Here's what I've found to work for me:

describe('MainController', function() {  
  beforeEach(module('hoogy'));
  var MainController;
  spyOn(_, 'debounce').and.callFake(function(func) {
    return function() {
      func.apply(this, arguments);
    };
  });
  beforeEach(inject(function($rootScope, $controller) {
    $scope = $rootScope.$new();
    MainController = $controller('MainController', {
      $scope: $scope
    });
    //hack: Not always required
    MainController = $scope.vm; 
  }));
  it('should find Def', function() {
    expect($scope.name).toEqual('Def');
  });
  it('should find Intel', function() {
    jasmine.clock().install();
    MainController.interval();
    jasmine.clock().tick(600);
    expect(MainController.name).toEqual('Intel');
    jasmine.clock().uninstall();
  });
  it('should find timeout', function() {
    jasmine.clock().install();
    MainController.timedout();
    jasmine.clock().tick(600);
    expect(MainController.name).toEqual('Timer');
    jasmine.clock().uninstall();
  });
  it('should find Bouncer', function() {
    jasmine.clock().install();
    MainController.debounced();
    jasmine.clock().tick(600);
    expect(MainController.name).toEqual('Bouncer');
    jasmine.clock().uninstall();
  });
  it('should find Throttler', function() {
    jasmine.clock().install();
    MainController.throttled();
    jasmine.clock().tick(600);
    expect(MainController.name).toEqual('Bouncer');
    jasmine.clock().uninstall();
  });
});

PS: timeout may need $timeout.flush(waitTime) to run in synchronous fashion.

Reference


Templates + UI

describe('admin edit user', function() {

  beforeEach(function() {
    browser().navigateTo('/admin/users/new');
    input('user.email').enter('admin@abc.com');
    input('user.password').enter('changeme');
    element('button.login').click();
  });

  it('enables the save button when the user info is filled in correctly', function() {
    expect(element('button.save:disabled').count()).toBe(1);
    input('user.email').enter('test@app.com');
    input('user.lastName').enter('Test');
    input('user.firstName').enter('App');
    input('user.password').enter('t');
    input('password').enter('t');
    expect(element('button.save:disabled').count()).toBe(0);
  });
});

Code came from this Art And Logic


Refactoring

Data Model Architecture

There is a noticeable lack of better model definition in AngularJS framework. In fact, any variable can become a Model. You just have to attach it the $scope, and there you go. I came across these links on how to refactor better your application to have a better model.

Better Controllers

After spending some time on a large scale angularjs project(day work), I realized that our controllers have grown into large chunk of code, with $scopes everywhere, logic everywhere, and the idea of skinny-controller-fat-model where quite a problem.

I was not alone to be frustrated by this aspect of AngularJS. Two guys who changed my life AngularWise, Todd Motto and John Papa.

Reveal Module Pattern

Custom matchers

Analytics

It can be tricky to add Google analytics to your SPA. There are good content about the subject out there. There are also alternatives(Mixpanel, Heap, Segment.io) that you may be interested to look into.

Curated content:

  1. Tracking Google Analytics Page Views with Angular.js
  2. Google Analytics trackevent in single-page web app
  3. Single Page Application Tracking - Web Tracking (analytics.js)
  4. Angularytics
  5. Angularytics: The solution to tracking page views and events in a SPA with AngularJS
  6. Analytics and tracking: Getting the most out of AngularJS
  7. Our platform was developed with AngularJS, single-page application. Will this have any negative impacts in terms of Google Analytics results and configuration? What things should we watch out for so we don't affect the script or generate polluted conversion data?
  8. The Complete Google Analytics Event Tracking Guide Plus 10 Amazing Examples

References