Over the last few years I’ve written several apps using Angular, but only after a project for work have I had to worry about unit testing them. Much like learning the framework itself, there are a number of techniques you need to pick up to be able to properly unit test an Angular app. Here are some common ones for testing controllers, services, and directives.

This post assumes the following:

  • You’re running AngularJS 1.3 or greater
  • You’re using Jasmine as your test framework
  • You’re using Karma as your test runner
  • You’re familiar with the above three things

Demo App

To-don't, an app for tracking things you shouldn't do

In order to write about testing, I need something to test! So I whipped up a to-do list app called “To-Don’t” that uses some common Angular features, including:

  • A controller ListController with the main application logic
  • A service TodoService for making HTTP calls to the backend
  • A custom ngEnter directive to enhance DOM capabilities

You can view the code on GitHub or try out the live demo.

Angular Mocks

The secret sauce to testing Angular apps is angular-mocks, a separate module not included in the main angular.js file. This is loaded in for unit tests only, as specified in the files section of karma.conf.js:

// list of files / patterns to load in the browser
files: [
  // External libs
  'http://code.jquery.com/jquery-2.1.4.min.js',
  'http://ajax.googleapis.com/ajax/libs/angularjs/1.4.3/angular.min.js',
  'http://ajax.googleapis.com/ajax/libs/angularjs/1.4.3/angular-mocks.js', // secret sauce!
  // Source
  'src/js/app.js',
  'src/js/ListController.js',
  'src/js/TodoService.js',
  'src/js/ngEnter.js',
  // Tests
  'test/spec/**/*-spec.js'
],

angular-mocks will expose a number of objects and methods required for writing unit tests.

Module Creation

The ToDont module, representing the To-Don’t Angular app, is initialized in app.js like this:

var ToDont = angular.module('ToDont', []);

In the unit tests, the ToDont module is loaded in a Jasmine beforeEach block using the module function:

beforeEach(module('ToDont'));

Controllers

Controllers are where most of the application logic lives. To-Don’t only has one controller, ListController, but your app might have one controller per route or page in your application.

Initialization and Dependency Injection

Angular uses dependency injection to provide dependencies to a component. For example, ListController is initialized with its dependencies using the controller method.

In ListController.js:

ToDont.controller('ListController', 
  ['$scope', '$timeout', 'TodoService', function($scope, $timeout, TodoService) {

    $scope.items = [];        // Array of to-do items
    $scope.newItem = '';      // New item to add to the list
    $scope.errorMsg = false;  // Error message

    // Get a list of saved items
    $scope.getItems = function() {
      TodoService.get().then(
        function(response) {
          $scope.items = response.data.items;
        },
        function(error) {
          $scope.errorMsg = error.error;
        }
      );
    };

    // Other controller methods go here

  }]
);

In the tests, the controller’s dependencies must be injected manually. This is done with angular-mock’s inject() function. Below ListController’s dependencies are manually injected into the test and a test controller is created, complete with a test $scope object.

In ListController-spec.js:

describe('ListController', function() {

  beforeEach(module('ToDont'));
  
  var TodoService, timeout, q, scope, controller;

  beforeEach(inject(function($rootScope, $controller, $timeout, $q, _TodoService_) {
    // Services
    TodoService = _TodoService_; // _'s are automatically unwrapped
    timeout = $timeout;
    q = $q;
    // Controller Setup
    scope = $rootScope.$new();
    controller = $controller('ListController', {
        $scope: scope,
        TodoService: TodoService
    });
  }));

  // Tests that use 'controller'

});

Dealing with $scope

Behavior is added to a controller by attaching properties and methods to the $scope object. When the app is running, actions such as method calls and changes to properties are handled by Angular’s own event processing capabilities. In the unit tests, however, Angular must be explicitly told that these actions are occurring. This is done with the $scope.$apply() method.

In ListController-spec.js:

describe('initialization', function() {

  it('initializes with proper $scope variables and methods', function() {
    scope.$apply(); // Let Angular know some changes have happened (in this case, the scope is created)
    expect(scope.items).toEqual([]);
    expect(scope.newItem).toEqual('');
    expect(scope.errorMsg).toEqual(false);
  });

});

$scope.$apply() can also be called with a function as a parameter. Code in the body of the function will be updated as if it were running in an Angular app.

it('is a test', function() {
  // Test stuff
  scope.$apply(function() {
    scope.getItems();
  });
  // More test stuff
});

Mocking Out Services

Because any services will be tested separately, they can be replaced in the controller unit tests with mocks. ListController utilizes the TodoService to interact with the backend API, so that will be mocked out. Methods of TodoService can be spied on individually and given mock behavior specific to each test.

In ListController-spec.js:

describe('getItems()', function() {

  it('successfully gets the list of items from the service', function() {
    // Mock implementation of TodoService.get()
    spyOn(TodoService, 'get').and.callFake(function() {
      var deferred = q.defer();
      deferred.resolve(testItems);
      return deferred.promise;
    });
    // Perform an action
    scope.$apply(function() {
      scope.getItems();
    });
    // Run expectations
    expect(TodoService.get).toHaveBeenCalled();
    expect(scope.items.length).toBe(testItems.length);
  });

  // Don't forget the negative test case for failed actions!

});

Services

Services are used to organize code into common functionality. In To-Don’t, the TodoService contains all the logic for updating the to-do list and is consumed by ListController. In this case, TodoService is also responsible for making requests to the backend API using the built-in $http service.

Initialization

Initializing a service is similar to creating a controller, using the either the service or factory method (there’s a difference) and injecting dependencies.

In TodoService.js:

ToDont.service('TodoService', ['$http', '$q', 'Config', function($http, $q, Config) {
  
  // BaseURL for the API
  this.baseUrl = Config.API_BASE_URL;

  // Get the list of items
  this.get = function() {
    var deferred = $q.defer();
    $http({
      url: this.baseUrl,
      method: "GET"
    }).success(function(data) {
      if (data.success === true) {
        deferred.resolve(data);
      }
      else {
        deferred.reject(data);
      }
    }).error(function(data) {
      deferred.reject(data);
    });
    return deferred.promise;
  };

  // Other service methods go here

}]);

In the tests the service instance is created using inject, similar to the controller tests.

In TodoService-spec.js:

describe('TodoService', function() {

  // Load the ToDont module
  beforeEach(module('ToDont'));

  var TodoService, httpBackend, q, testUrl, testItem, testItems;

  beforeEach(inject(function(_TodoService_, $httpBackend, $q) {
    testUrl = 'API_BASE_URL';
    // Test data
    testItem = {id:1,desc:'test item',complete:false};
    testItems = [testItem];
    // Service instance and dependencies
    TodoService = _TodoService_;
    httpBackend = $httpBackend;
    q = $q;
  }));

  // Tests go here

});

The TodoService instance is now ready to be tested.

Mocking HTTP Requests

In the controller unit tests the focus was the controller logic, so the TodoService calls were mocked out. In the service unit tests the focus is the TodoService logic, so the HTTP calls it makes will need to be mocked out; the backend itself doesn’t need to be tested (at least not here).

angular-mocks provides a mock implementation of $httpBackend, acting like a Jasmine spy for HTTP requests. In the tests, $httpBackend is told to expect certain requests and respond to them as appropriate. Below is an example of testing a successful GET request.

In TodoService-spec.js:

describe('get()', function() {

  it('gets the list of todo items', function() {
    var promise, response, result;
    // Make the request and implement a fake success callback
    promise = TodoService.get();
    promise.then(function(data) {
      result = data;
    });
    response = {
      success: true,
      data: {
        items: testItems
      }
    };
    httpBackend.expectGET(testUrl).respond(200, response); // Expect a GET request and send back a canned response
    httpBackend.flush(); // Flush pending requests
    expect(result).toEqual(response);
    expect(result.data.items).toEqual(testItems);
  });

  // Don't forget to add negative test cases for failed requests!

});

In between tests, make sure to clear $httpBackend to catch any unexpected results:

afterEach(function() {
  httpBackend.verifyNoOutstandingExpectation();
  httpBackend.verifyNoOutstandingRequest();
});

Directives

Directives are Angular’s way of teaching the DOM new tricks. To-Don’t expands on the DOM’s native capabilities with an ngEnter directive that runs a callback function when the enter key is pressed within an input field. The directive is created using the directive function and returns a link function.

In ngEnter.js:

ToDont.directive('ngEnter', function() {
  function (scope, element, attrs) {
    element.bind('keydown keypress', function(e) {
      if (e.which === 13) {
        scope.$apply(function() {
          scope.$eval(attrs.ngEnter);
        });
        e.preventDefault();
      }
    });
  };
});

Once a directive is defined, it is included in the HTML for the application:

<p class="add">Add new: 
  <input type="text" ng-model="newItem" ng-enter="addItem()" placeholder="New item">
</p>

This binds the addItem() method of ListController as the callback when the enter key is pressed on the input. All the magic of linking and binding the callback is handled by Angular.

Creating the Test Fixture

The directive can be tested by creating a fixture to interact with. The HTML will need to be compiled and registered with Angular manually using the element() method and $compile service.

In ngEnter-spec.js:

describe('ngEnter directive', function() {

  beforeEach(module('ToDont'));

  var testFixture = '<input type="text" ng-enter="enter()"/>',
      scope, compile, element, compiled, event;

  beforeEach(inject(function($rootScope, $compile) {
    scope = $rootScope.$new();
    compile = $compile;
    // Create the test fixture
    scope.enter = function() {};
    spyOn(scope, 'enter');
    element = angular.element(testFixture);
    compiled = compile(element)(scope);
    scope.$apply();
  }));

  // Tests go here

});

The final compiled element can now be tested by simulating user interaction. The test below simulates the user pressing the enter key.

In ngEnter-spec.js:

it('should call scope.enter() on enter key press', function() {
  event = $.Event('keypress', {which:13});
  $(compiled).trigger(event);
  scope.$apply();
  expect(scope.enter).toHaveBeenCalled();
});

Resources

There’s much more to unit testing Angular apps than this post covers. Here are some materials for further reading:

Don’t forget to check out the source for To-Don’t on GitHub and try out the live demo!