A. Sharif

Testing Components in AngularJS

15 Feb 2015

Introduction

There seems be a trend towards component based thinking in AngularJS. Especially rethinking the ng-controller/scope/view approach has been the basis for a lot of articles, post, talks and discussions in the past couple of months.

This post is based on the previous Component Based Thinking in AngularJS and is quasi a follow up where the focus is solely on testing. You can find the original code here.

The following section will assume prior knowledge on testing in general and Jasmine and Karma in specific and will not go into detail on how to install or setup Karma and Jasmine. More information on setting up the testing environment can be found here for example.

Testing the <item> Component

The original example consisted of an <itemsContainer> that composed <itemsList> and <searchBox> components. Both the <itemsList> and the <searchBox> component also composed <item> components.

The low level item directive can easily be tested as we don't have any controller logic to consider, all the component does is receive an object with a name and an activity status. Based on the given item object it renders a title consisting of the item name as well as a checkbox which is either checked or unchecked depending on the active property.

So the first step is to create the test skeleton. By using the beforeEach function we can import the app module, create a new scope and compile the element with the proper scope. This has the benefit of only needing to define those steps once instead of having to redefine the setup in every single test. See the code below for clarification.

describe('Item component', function() {
  var element, scope;

  beforeEach(module('app'));

  beforeEach(inject(function(_$rootScope_, _$compile_) {
    $compile = _$compile_;
    $rootScope = _$rootScope_;

    scope = $rootScope.$new();

    element = angular.element('<item data-set="item" on-click="ctrl.callback({item: item})"></item>');

    $compile(element)(scope);

  }));

  it('should display the controller defined title');
  it('should call the controller defined callback');
});

Currently there are two formulated tests that should describe the expected <item> component behaviour. The first test needs to verify that a passed item's name is really rendered when calling the item component. By adding an item to the scope and calling scope.$digest(), which simply updates the scope properties on the previously created element, we are able to verify if the expected title has been rendered via Jasmine's toContain() method.

  it('should display the controller defined title', function() {
    var expected = 'Some rendered text';
    scope.item = {
      name: expected,
      active: false
    };

    scope.$digest();

    expect(element.text()).toContain(expected);
  });

The second test needs to simulate a click behaviour as it expects a pre defined callback to be triggered when clicking on an item checkbox. This test setup also loads jQuery so simulating a click on an element can simply be achieved by selecting an element and applying the click() call.

 it('should call the controller defined callback', function() {
    scope.ctrl = {
      callback: jasmine.createSpy('callback')
    };

    scope.$digest();

    element.find('input[type=checkbox]').eq(0).click();

    expect(scope.ctrl.callback).toHaveBeenCalled();
  });

It should be noted that the callback function was mocked by using Jasmine's createSpy method. This approach comes with the benefit that we can test if the callback has been called by simply using the toHaveBeenCalled() method.

Testing the <itemsList> and <searchBox> Component

The tests for the itemsList and searchBox components are very similar to the previously created item tests. One important difference though is the fact that both the itemsList as well as the searchBox component rely on an external template defined via the directive's templateUrl property.

There are different approaches to include an external template into the to be tested directive.

One possibility is to setup Karma to serve the templates via the karma-ng-html2js-preprocessor. More information can be found here. The other approach, which we will actually use in this example, is to add a string template into $templateCache inside the beforeEach function. This ensures that the template is available when the directive test runs. More information on $tempateCache can be found here.

  beforeEach(inject(function($templateCache) {
    $templateCache.put('items-list.html',
      '<div class="items-list">' +
      '<h3></h3>' +
      '<span ng-if="ctrl.items.length == 0">No items available.</span> ' +
      '<ul class="items"> ' +
      '<li ng-repeat="item in ctrl.items"> ' +
      '<item data-set="item" on-click="ctrl.onClick({item: item})"></item> ' +
      '</li> ' +
      '</ul> ' +
      '</div>'
    );
  }));

The <itemsList> component tests are straight forward. The scope is extended with an items array to verify that the items are really rendered as single item elements. The rest is similar to the previous section, including testing if a passed in title is really rendered. The full code below:

One interesting aspect to consider when testing the <searchBox> component is a that user input has to be simulated. This is due to the fact that a callback is triggered via the input element's ng-change property. Simulating the input can be achieved by accessing the input's ngModelController and changing the view value by using $setViewValue().

  it('changing the search string should trigger the controller defined callback', function() {
      scope.ctrl = {
          updateFilter : jasmine.createSpy('updateFilter')
      };

      scope.$digest();

      var input = element.find('#search-box').eq(0);
      var ngModelCtrl = input.controller('ngModel');
      ngModelCtrl.$setViewValue('view');

      expect(scope.ctrl.updateFilter).toHaveBeenCalledWith('view');
  });

Testing the ItemsContainerController

The ItemsContainer composes all the previously tested components.

Testing the ItemsContainerController ensures that the data updates are handled as expected and that the lower level components receive the correct data to function properly. Further the container also gets an ItemsService injected, which we will mock when testing the overall component behaviour. Mocking can be achieved by creating a dummy service object for example. The mocked object will always return the same set of items.

  beforeEach(function() {
    items = [{
      id: 1,
      name: 'view',
      active: true
    }, {
      id: 2,
      name: 'model',
      active: true
    }, {
      id: 3,
      name: 'scope',
      active: false
    }];

    mockedItemsService = {
      fetchAll: function() {
        return items;
      },
      update: function(item) {
        return items;
      }
    };
  });

One interesting aspect is that we can test a 'controller as' controller by also using the 'controller as' syntax when creating the controller via $controller.

  beforeEach(inject(function($controller, _$rootScope_, _$compile_) {
    $compile = _$compile_;
    $rootScope = _$rootScope_;

    scope = $rootScope.$new();
    controller = $controller('ItemsContainerController as ctrl', {
      $scope: scope,
      ItemsService: mockedItemsService
    });
  }));

The rest is obvious, we are testing the default behaviour when the itemsContainer component is rendered as well as the updateFilter and switchStatus functions.

The full code is available here.

Links

Component Based Thinking in AngularJS

Component-Based AngularJS Directives

Directive components in ng

Jasmine

karma-runner/karma-jasmine on GitHub

$tempateCache