AngularJS Testing Tips: Testing Directives

Share this article

Unit tests are an essential part of software development as they help you in releasing less buggy code. Testing is one of the several things that one has to do to improve code quality. AngularJS is created with testing in mind and any code written on top of the framework can be tested easily. In my last article on testing, I covered Unit testing Controllers, Services and Providers. This article continues the discussion on testing with directives. Directives are different from other components because they aren’t used as objects in the JavaScript code, but in HTML templates of the application. We write directives to perform DOM manipulations and we can’t ignore them in unit tests as they play an important role. Besides, they directly affect the usability of the application. I encourage you to check out the past article on Mocking dependencies in AngularJS tests, as we will be using some of the techniques from that article here. In case you want to play with the code developed in this tutorial, you can take a look at the GitHub repository I set up for you.

Testing Directives

Directives are the most important and most complex components in AngularJS. Testing directives is tricky, as they are not called like a function. In applications, the directives are declaratively applied on the HTML template. Their actions are executed when the template is compiled and a user interacts with the directive. When performing unit tests, we need to automate the user actions and manually compile the HTML in order to test the functionality of the directives.

Setting up Objects to Test a Directive

Just like testing any piece of logic in any language or using any framework, we need to get references of the objects needed before starting to test a directive. The key object to be created here is an element containing the directive to be tested. We need to compile a piece of HTML with the directive specified in it to get the directive into action. For instance, consider the following directive:
angular.module('sampleDirectives', []).directive('firstDirective', function() {
  return function(scope, elem){
    elem.append('<span>This span is appended from directive.</span>');
  };
});
Lifecycle of the directive will be kicked in, and the compile and link functions will be executed. We can manually compile any HTML template using the $compile service. The following beforeEach block compiles the above directive:
var compile, scope, directiveElem;

beforeEach(function(){
  module('sampleDirectives');
  
  inject(function($compile, $rootScope){
    compile = $compile;
    scope = $rootScope.$new();
  });
  
  directiveElem = getCompiledElement();
});

function getCompiledElement(){
  var element = angular.element('<div first-directive></div>');
  var compiledElement = compile(element)(scope);
  scope.$digest();
  return compiledElement;
}
On compilation, the lifecycle of the directive is kicked in. After the next digest cycle, the directive object would be in the same state as it appears on a page. If the directive depends on any service to achieve its functionality, these services have to be mocked before compiling the directive, so that calls to any service methods can be inspected in the tests. We’ll see an example in the next section. Link function is the most used property of the directive definition object (DDO). It contains most of the core logic of the directive. This logic includes simple DOM manipulations, listening to pub/sub events, watching for change of an object or an attribute, calling services, handling UI events, and so on. We will try to cover most of these scenarios.

DOM Manipulation

Let’s start with case of the directive defined in the previous section. This directive adds a span element to the content of the element on which the directive is applied. It can be tested by finding the span inside the directive. The following test case asserts this behavior:
it('should have span element', function () {
  var spanElement = directiveElem.find('span');
  expect(spanElement).toBeDefined();
  expect(spanElement.text()).toEqual('This span is appended from directive.');
});

Watchers

As directives work on current state of scope, they should have watchers to update the directive when state of the scope changes. Unit test for the watcher has to manipulate data and force the watcher to run by calling $digest and it has to check the state of directive after the digest cycle. The following code is a slightly modified version of the above directive. It uses a field on scope to bind text inside the span:
angular.module('sampleDirectives').directive('secondDirective', function(){
  return function(scope, elem){
    var spanElement = angular.element('<span>' + scope.text + '</span>');
    elem.append(spanElement);

    scope.$watch('text', function(newVal, oldVal){
      spanElement.text(newVal);
    });
  };
});
Testing this directive is similar to the first directive; except it should be validated against data on scope and should be checked for update. The following test case validates if the state of the directive changes:
it('should have updated text in span', function () 
  scope.text = 'some other text';
  scope.$digest();
  var spanElement = directiveElem.find('span');
  expect(spanElement).toBeDefined();
  expect(spanElement.text()).toEqual(scope.text);
});
The same technique can be followed to test observers on attributes as well.

DOM Events

The importance of events in any UI based application forces us to ensure that they are working correctly. One of the advantages of JavaScript-based applications is that most of the user interaction is testable through APIs. Events can be tested using the APIs. We can trigger events using the jqLite API and test logic inside the event. Consider the following directive:
angular.module('sampleDirectives').directive('thirdDirective', function () {
  return {
      template: '<button>Increment value!</button>',
      link: function (scope, elem) {
        elem.find('button').on('click', function(){
          scope.value++;
        });
      }
    };
  });
The directive increments the value of the value property by one on every click of the button element. The test case for this directive has to trigger the click event using jqLite’s triggerHandler
and then check if the value is incremented. This is how you test the previous code:
it('should increment value on click of button', function () {
  scope.value=10;
  var button = directiveElem.find('button');

  button.triggerHandler('click');
  scope.$digest();

  expect(scope.value).toEqual(11);
});
In addition to the cases mentioned here, the link function contains logic involving the interaction with services or publishing /subscribing scope events. To test these cases, you can follow the techniques discussed in my previous post. The same techniques can be applied here too. The compile block has responsibilities similar to link. The only difference is that the compile block can’t use or manipulate scope, as the scope is not available by the time compile runs. DOM updates applied by the compile block can be tested by inspecting HTML of the rendered element.

Testing Directive’s Template

A template can be applied to a directive in two ways: using an inline template or using a file. We can verify if the template is applied on a directive and also if the template has certain elements or directives in it. A directive with inline template is easier to test as it’s available in the same file. Testing a directive with template referred from a file is tricky, as the directive makes an $httpBackend request to the templateUrl. Adding this template to $templateCache makes the task of testing easier and the template will be easy to share. This can be done using the grunt-html2js grunt task. grunt-html2js is very easy to configure and to use. It needs the source path(s) of the html file(s) and a destination path where the resultant script has to be written. The following is the configuration used in the sample code:
html2js:{
  main: {
    src: ['src/directives/*.html'],
    dest: 'src/directives/templates.js'
  }
}
Now, all we need to do is to refer the module generated by this task in our code. By default, name of the module generated by grunt-html2js is templates-main but you can modify it. Consider the following directive:
angular.module('sampleDirectives', ['templates-main'])
.directive('fourthDirective', function () {
  return {
    templateUrl: 'directives/sampleTemplate.html'
  };
});
And the content of template:
<h3>Details of person {{person.name}}<h3>
<another-directive></another-directive>
The template has another-directive element, which is another directive and it’s an important part of the template. Without anotherDirective directive, fourthDirective won’t work as expected. So, we have to validate the followings after the directive is compiled:
  1. If the template is applied inside the directive element
  2. If the template contains another-directive element
These are the tests to demonstrate these cases:
it('should applied template', function () {
  expect(directiveElem.html()).not.toEqual('');
});

it('should have another-person element', function () {
  expect(directiveElem.find('another-directive').length).toEqual(1);
});
You don’t need to write test for every single element in the directive’s template. If you feel that a certain element or directive is mandatory in the template, and without that the directive would not be complete, add a test to check for the existence of such component. Doing so, your test will complain if someone accidentally removes it.

Testing Directive’s Scope

A directive’s scope can be one of the following:
  1. Same as scope of surrounding element
  2. Inherited from scope of surrounding element
  3. Isolated scope
In the first case, you may not want to test the scope as the directive is not supposed to modify state of the scope when it uses the same scope. But in other cases, the directive may add some fields to the scope that drive behavior of the directive. We need to test these cases. Let’s take an example of a directive using isolated scope. Following is the directive that we have to test:
angular.module('sampleDirectives').directive('fifthDirective', function () {
  return {
    scope:{
      config: '=',
      notify: '@',
      onChange:'&'
    }
  }
};
})
In the tests of this directive, we need to check if the isolated scope has all three properties defined and if they are assigned with the right values. In this case, we need to test the following cases:
  1. config property on isolated scope should be same as the one on scope and is two-way bound
  2. notify property on isolated scope should be one-way bound
  3. onChange property on isolated scope should be a function and the method on scope should be called when it is invoked
The directive expects something on the surrounding scope, so it needs a slightly different set up and we also need to get a reference of the isolated scope. The snippet below prepares the scope for the directive and compiles it:
beforeEach(function() {
  module('sampleDirectives');
  inject(function ($compile, $rootScope) {
    compile=$compile;
    scope=$rootScope.$new();
    scope.config = {
      prop: 'value'
    };
    scope.notify = true;
    scope.onChange = jasmine.createSpy('onChange');
  });
  directiveElem = getCompiledElement();
});

function getCompiledElement(){
  var compiledDirective = compile(angular.element('<fifth-directive config="config" notify="notify" on-change="onChange()"></fifth-directive>'))(scope);
  scope.$digest();
  return compiledDirective;
Now that we have the directive ready, let’s test if the isolated scope is assigned with the right set of properties.
it('config on isolated scope should be two-way bound', function(){
  var isolatedScope = directiveElem.isolateScope();

  isolatedScope.config.prop = "value2";

  expect(scope.config.prop).toEqual('value2');
});

it('notify on isolated scope should be one-way bound', function(){
  var isolatedScope = directiveElem.isolateScope();

  isolatedScope.notify = false;

  expect(scope.notify).toEqual(true);
});

it('onChange should be a function', function(){
    var isolatedScope = directiveElem.isolateScope();

    expect(typeof(isolatedScope.onChange)).toEqual('function');
});

it('should call onChange method of scope when invoked from isolated scope', function () {
    var isolatedScope = directiveElem.isolateScope();
    isolatedScope.onChange();

    expect(scope.onChange).toHaveBeenCalled();
});

Testing Require

A directive may strictly or optionally depend on one or a set of other directives. For this reason, we have some interesting cases to test:
  1. Should throw error if a strictly required directive is not specified
  2. Should work if a strictly required directive is specified
  3. Should not throw error if an optionally required directive is not specified
  4. Should interact with controller of optional directive if it is found
The directive below requires ngModel and optionally requires form in a parent element:
angular.module('sampleDirectives').directive('sixthDirective', function () {
    return {
      require: ['ngModel', '^?form'],
      link: function(scope, elem, attrs, ctrls){
        if(ctrls[1]){
          ctrls[1].$setDirty();
      }
    }
  };
});
As you can see, the directive interacts with the form controller only if it is found. Though the example doesn’t make much sense, it gives the idea of the behavior. The tests for this directive, covering the cases listed above, are shown below:
function getCompiledElement(template){
  var compiledDirective = compile(angular.element(template))(scope);
  scope.$digest();
  return compiledDirective;
}

it('should fail if ngModel is not specified', function () {
  expect(function(){
    getCompiledElement('<input type="text" sixth-directive />');
  }).toThrow();
});

it('should work if ng-model is specified and not wrapped in form', function () {
  expect(function(){
    getCompiledElement('<div><input type="text" ng-model="name" sixth-directive /></div>');
  }).not.toThrow();
});

it('should set form dirty', function () {
  var directiveElem = getCompiledElement('<form name="sampleForm"><input type="text" ng-model="name" sixth-directive /></form>');

  expect(scope.sampleForm.$dirty).toEqual(true);
});

Testing Replace

Testing replace is very simple. We just have to check if the directive element exists in the compiled template. This is how you do that:
//directive
angular.module('sampleDirectives').directive('seventhDirective', function () {
  return {
    replace: true,
    template: '<div>Content in the directive</div>'
  };
});

//test
it('should have replaced directive element', function () {
  var compiledDirective = compile(angular.element('<div><seventh-directive></seventh-directive></div>'))(scope);
  scope.$digest();

  expect(compiledDirective.find('seventh-directive').length).toEqual(0);
});

Testing Transclude

Transclusion has two cases: transclude set to true and transclude set to an element. I haven’t seen many use cases of transclude set to element, so we will only discuss the case of transclude set to true. We got to test the following to check if the directive supports transcluded content:
  1. If the template has an element with ng-transclude directive on it
  2. If the content is preserved
To test the directive, we need to pass some HTML content inside the directive to be compiled and then check for the above cases. This is a directive using transclude and its test:
//directive
angular.module('sampleDirectives').directive('eighthDirective', function(){
  return{
    transclude: true,
    template:'<div>Text in the directive.<div ng-transclude></div></div>'
  };
});

//test
it('should have an ng-transclude directive in it', function () {
    var transcludeElem = directiveElem.find('div[ng-transclude]');
    expect(transcludeElem.length).toBe(1);
});

it('should have transclude content', function () {
    expect(directiveElem.find('p').length).toEqual(1);
});

Conclusion

As you’ve seen in this article, directives are harder to test when compared with other concepts in AngularJS. At the same time, they can’t be ignored as they control some of the important parts of the application. AngularJS’s testing ecosystem makes it easier for us to test any piece of a project. I hope that thanks to this tutorial you are more confident to test your directives now. Let me know your thoughts in comment section. In case you want to play with the code developed in this tutorial, you can take a look at the GitHub repository I set up for you.

Frequently Asked Questions (FAQs) on Angular Testing and Directives

What is the importance of testing directives in Angular?

Testing directives in Angular is crucial as they are responsible for manipulating the Document Object Model (DOM), which is a programming interface for web documents. Directives define the application’s behavior and structure, making them a critical part of the application. By testing them, developers can ensure that the application behaves as expected, reducing the chances of bugs and errors. It also helps in maintaining the code quality and enhancing the application’s performance.

How can I test a directive in Angular?

To test a directive in Angular, you can use the Angular testing utilities and Jasmine framework. First, you need to create a test bed for your directive and compile it. Then, you can create a fixture and use it to access the directive instance. You can then perform various tests on the directive, such as checking its properties, methods, and DOM manipulation.

What is the difference between href and ng-href in Angular?

The main difference between href and ng-href in Angular is that href is a standard HTML attribute, while ng-href is an Angular directive. The href attribute is used to specify the URL of the page the link goes to. On the other hand, ng-href is used when you have Angular expressions in your URL, which need to be evaluated before the actual URL is constructed. This ensures that the link is always correct, even if the Angular expressions change.

How can I use ng-href in Angular?

To use ng-href in Angular, you need to include it as an attribute in your anchor tag. The value of ng-href should be an Angular expression that evaluates to the URL you want the link to point to. For example, <a ng-href="{{myUrl}}">Link</a>. Here, myUrl is an Angular expression that should evaluate to the actual URL.

What is unit testing in Angular?

Unit testing in Angular is a testing method where individual parts of the application, such as components, services, and directives, are tested in isolation. This helps in identifying and fixing bugs at an early stage of development. Angular provides a testing framework called Jasmine and a test runner called Karma for unit testing.

How can I perform unit testing in Angular?

To perform unit testing in Angular, you can use the Jasmine testing framework and Karma test runner provided by Angular. First, you need to create a spec file for the component or service you want to test. Then, you can write test cases in the spec file using Jasmine syntax. Finally, you can run the tests using the Karma test runner.

What is the role of directives in Angular?

Directives in Angular are used to manipulate the DOM and extend the functionality of HTML. They can change the appearance, behavior, or layout of DOM elements. Angular provides several built-in directives, such as ngModel, ngIf, and ngFor. You can also create custom directives to suit your specific needs.

How can I create a custom directive in Angular?

To create a custom directive in Angular, you need to use the @Directive decorator and provide a selector for the directive. The selector is used to identify the directive in the HTML. You can then define the behavior of the directive in the directive’s class. For example, you can manipulate the DOM, bind data, or handle events.

What is the Angular testing environment?

The Angular testing environment consists of the Jasmine testing framework, Karma test runner, and Angular testing utilities. Jasmine provides a syntax for writing test cases, Karma is used to run the tests, and Angular testing utilities provide functions for creating test beds, fixtures, and spies.

How can I set up the Angular testing environment?

To set up the Angular testing environment, you need to install Jasmine, Karma, and Angular testing utilities. You can install them using npm, the Node.js package manager. Once installed, you can configure Karma by creating a karma.conf.js file in your project root. You also need to create spec files for your components, services, and directives where you will write your test cases.

Rabi Kiran (a.k.a. Ravi Kiran) is a developer working on Microsoft Technologies at Hyderabad. These days, he is spending his time on JavaScript frameworks like Angular JS, latest updates to JavaScript in ES6 and ES7, Web Components, Node.js and also on several Microsoft technologies including ASP.NET 5, SignalR and C#. He is an active blogger, an author at SitePoint and at DotNetCurry. He is rewarded with Microsoft MVP (ASP.NET/IIS) and DZone MVB awards for his contribution to the community.

angularangular directivesangularjsAurelioDjavascriptTestingunit testunit testing
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week