Creating unit tests in AngularJS

Unit testing can be one of the most disliked part of the software development. John Papa came forward and stated that he dislikes writing tests because of the amount of setup required to write the first "it" (source: http://devchat.tv/adventures-in-angular/025-aia-testing-with-ward-bell). I can definitely relate and I figure I should share some tips
to reduce the boilerplate for writing tests.

Why we should

Writing tests help write more testable code which usually translates to more simple code and allows for easier refactoring. It also instills confidence in the developers to deploy their code that are unit tested.

  • Reduce time to first "it"

  • Maintainability

  • Readability while being more declarative instead of imperative

Tooling

  • Karma
  • Jasmine

Ways to reduce boilerplate

  • if using Jasmine, nested describes helps write clearer tests. By nesting describes, we can bundle similar tests as well as bundle similar boilerplate in the beforeEach and write leaner "it" statements. (http://devchat.tv/adventures-in-angular/026-aia-testing-tools)
  • reduce mocks (refer to post on TDD is dead?)
    • Less global states
    • More modular code
  • automate ways to run tests
    • karma TDD, by using PhantomJS. Setup to run on file save.

Plugins

  • promote ng-html2js
  • promote your ng-request2js
  • promote your html-to-json-array
Testing directives (general)
//directive someDirective
angular.module('someDirective')  
  .directive('someDirective', someDirective);
function someDirective() {  
  return {
    restrict: 'E',
    scope: {
      personName: '='
    },
    template: '<div>{{welcomeMessage}}</div>',
    link: function(scope, element, attrs) {
      scope.welcomeMessage = 'Hello ' + scope.personName;
    }
  }
};
// typical Directive boilerplate should go here
describe('someDirective', function () {  
  beforeEach(module('app'));
  var $scope, $compile, $rootScope, element;

  beforeEach(inject(function($injector) {
    $rootScope = $injector.get('$rootScope');
    $compile = $injector.get('$compile');
    $scope = $rootScope.$new();

    element = angular.element('<some-directive person-name="John"></some-directive>');
    $compile(element)($scope);
  }));

  it('should welcome the user', function() {
      expect(element.html()).toContain('Hello John');
  });

});
Testing directives (controller)
// directive someDirective
angular.module('app', []);  
angular.module('app')  
  .directive('someDirective', someDirective);

function someDirective() {  
  return {
    restrict: 'E',
    scope: {},
    template: '<div>{{welcomeMessage}}</div>',
    controller: function($scope) {
      $scope.welcome = function(name) {
        $scope.welcomeMessage = 'Hello ' + name;
      }
    }
  }
}

test directive controller scope

describe('someDirective', function () {  
  beforeEach(module('app'));
  var $scope, $compile, $rootScope, element;

  beforeEach(inject(function($injector) {
    $rootScope = $injector.get('$rootScope');
    $compile = $injector.get('$compile');
    $scope = $rootScope.$new();

    element = angular.element('<some-directive ></some-directive>');
    $compile(element)($scope);
  }));

  it('should welcome the user', function() {
      var scope = element.isolateScope();
      scope.welcome('John');
      $scope.$digest();
      expect(element.html()).toContain('Hello John');
  });

test directive controller this

// directive someDirective
angular.module('app', []);  
angular.module('app')  
  .directive('someDirective', someDirective);

function someDirective() {  
  return {
    restrict: 'E',
    scope: {},
    template: '<div>{{welcomeMessage}}</div>',
    controller: function($scope) {
      this.welcome = function(name) {
        $scope.welcomeMessage = 'Hello ' + name;
      }
    }
  }
};
describe('someDirective', function () {  
  beforeEach(module('app'));
  var $scope, $compile, $rootScope, element, controller;

  beforeEach(inject(function($injector) {
    $rootScope = $injector.get('$rootScope');
    $compile = $injector.get('$compile');
    $scope = $rootScope.$new();

    element = angular.element('<some-directive ></some-directive>');
    $compile(element)($scope);

    $rootScope.$apply();
    controller = element.controller('someDirective');
  }));

  it('should welcome the user', function() {
      controller.welcome('John');
      $scope.$digest();
      expect(element.html()).toContain('Hello John');
  });

Testing Services

// someService code here
angular.module('app', []);  
angular.module('app')  
  .factory('someService', someService);

function someService() {  
  var welcomeMessage = '';
  var MessageCreator = function() {
    this.welcome = function(name) {
      welcomeMessage = 'Hello ' + name;
    };

    this.getWelcomeMessage = function() {
      return welcomeMessage;
    };
  }
  return MessageCreator;
};
// test code here
describe('someService', function() {  
  beforeEach(module('app'));
  var $rootScope, $scope, $scope;
  beforeEach(inject( function ($injector) {
    $rootScope = $injector.get('$rootScope');
    someService = $injector.get('someService');
    $scope = $rootScope.$new();
  }));

  it('should create new someService object', function() {
    var service = new someService();
    expect(someService).toBeDefined();
    expect(typeof someService).toBe('object');
  });

  it('should create a welcome message', function() {
    var service = new someService();
    service.welcome('John');
    expect(service.getWelcomeMessage()).toEqual('Hello John');
  });
}

Testing Controllers

// someController code here

angular.module('app', []);  
angular.module('app')  
  .controller('SomeController', SomeController);
function SomeController($scope) {  
  $scope.welcome = "Welcome " + $scope.name;
}
// test code here
describe('someController', function(){  
  beforeEach(module('app'));
  var scope, ctrl;

  beforeEach(inject(function($controller, $rootScope) {
    $cope = $rootScope.$new();
    ctrl = $controller(someController, { $scope: scope });
  }));

  it('should change welcome message when name is set', function() {
    scope.name = "John";
    scope.$digest();
    expect(scope.welcome).toBe("Welcome John");
  });
});
Comments powered by Disqus