Vous êtes sur la page 1sur 16

AngularJS Testing Tips: Testing Directives

MENU

JavaScript

AngularJS Testing Tips: Testing


Directives

Tweet

Subscribe

Ravi

Published May 20, 2015


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 arent used as objects in the JavaScript code, but in HTML
templates of the application. We write directives to perform DOM manipulations and we cant
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.

http://www.sitepoint.com/...g-directives/?utm_content=buffer0b618&utm_medium=social&utm_source=facebook.com&utm_campaign=buffer[5/27/2015 4:20:10 AM]

AngularJS Testing Tips: Testing Directives

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 specifed in it to get the directive into action. For instance, consider
the following directive:

angular.module('sampleDirectives', []).directive('frstDirective',
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');

http://www.sitepoint.com/...g-directives/?utm_content=buffer0b618&utm_medium=social&utm_source=facebook.com&utm_campaign=buffer[5/27/2015 4:20:10 AM]

AngularJS Testing Tips: Testing Directives

inject(function($compile, $rootScope){
compile = $compile;
scope = $rootScope.$new();
});
directiveElem = getCompiledElement();
});
function getCompiledElement(){
var element = angular.element('<div frst-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. Well see an example in the next section.

Testing Link Function


Link function is the most used property of the directive defnition 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

Lets start with case of the directive defned 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 fnding the span inside the directive. The following test case asserts this behavior:

it('should have span element', function () {


var spanElement = directiveElem.fnd('span');

http://www.sitepoint.com/...g-directives/?utm_content=buffer0b618&utm_medium=social&utm_source=facebook.com&utm_campaign=buffer[5/27/2015 4:20:10 AM]

AngularJS Testing Tips: Testing Directives

expect(spanElement).toBeDefned();
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 modifed version of the above directive. It uses a feld 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 frst 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.fnd('span');
http://www.sitepoint.com/...g-directives/?utm_content=buffer0b618&utm_medium=social&utm_source=facebook.com&utm_campaign=buffer[5/27/2015 4:20:10 AM]

AngularJS Testing Tips: Testing Directives

expect(spanElement).toBeDefned();
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.fnd('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 jqLites
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.fnd('button');

http://www.sitepoint.com/...g-directives/?utm_content=buffer0b618&utm_medium=social&utm_source=facebook.com&utm_campaign=buffer[5/27/2015 4:20:10 AM]

AngularJS Testing Tips: Testing Directives

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 cant 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 Directives Template


A template can be applied to a directive in two ways: using an inline template or using a fle.
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 its available in the same fle. Testing a
directive with template referred from a fle 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 confgure and to use. It needs the source path(s) of the html

fle(s) and a destination path where the resultant script has to be written. The following is the
confguration used in the sample code:

html2js:{
main: {
src: ['src/directives/*.html'],
dest: 'src/directives/templates.js'
}
http://www.sitepoint.com/...g-directives/?utm_content=buffer0b618&utm_medium=social&utm_source=facebook.com&utm_campaign=buffer[5/27/2015 4:20:10 AM]

AngularJS Testing Tips: Testing Directives

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 its an
important part of the template. Without anotherDirective directive, fourthDirective
wont 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('');
});

http://www.sitepoint.com/...g-directives/?utm_content=buffer0b618&utm_medium=social&utm_source=facebook.com&utm_campaign=buffer[5/27/2015 4:20:10 AM]

AngularJS Testing Tips: Testing Directives

it('should have another-person element', function () {


expect(directiveElem.fnd('another-directive').length).toEqual(1);
});

You dont need to write test for every single element in the directives 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 Directives Scope


A directives 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 frst 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 felds to the scope that drive behavior of the directive. We need to test these cases.
Lets take an example of a directive using isolated scope. Following is the directive that we
have to test:

angular.module('sampleDirectives').directive('ffthDirective',
function () {
return {
scope:{
confg: '=',
notify: '@',
onChange:'&'
}
}
};
})

http://www.sitepoint.com/...g-directives/?utm_content=buffer0b618&utm_medium=social&utm_source=facebook.com&utm_campaign=buffer[5/27/2015 4:20:10 AM]

AngularJS Testing Tips: Testing Directives

In the tests of this directive, we need to check if the isolated scope has all three properties
defned and if they are assigned with the right values. In this case, we need to test the
following cases:
1. confg 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.confg = {
prop: 'value'
};
scope.notify = true;
scope.onChange = jasmine.createSpy('onChange');
});
directiveElem = getCompiledElement();
});
function getCompiledElement(){
var compiledDirective = compile(angular.element('<ffth-directive
confg="confg" notify="notify" on-change="onChange()"></ffthdirective>'))(scope);
scope.$digest();
return compiledDirective;

Now that we have the directive ready, lets test if the isolated scope is assigned with the right
set of properties.

http://www.sitepoint.com/...g-directives/?utm_content=buffer0b618&utm_medium=social&utm_source=facebook.com&utm_campaign=buffer[5/27/2015 4:20:10 AM]

AngularJS Testing Tips: Testing Directives

it('confg on isolated scope should be two-way bound', function(){


var isolatedScope = directiveElem.isolateScope();
isolatedScope.confg.prop = "value2";
expect(scope.confg.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 specifed
2. Should work if a strictly required directive is specifed

http://www.sitepoint.com/...g-directives/?utm_content=buffer0b618&utm_medium=social&utm_source=facebook.com&utm_campaign=buffer[5/27/2015 4:20:10 AM]

AngularJS Testing Tips: Testing Directives

3. Should not throw error if an optionally required directive is not specifed


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

http://www.sitepoint.com/...g-directives/?utm_content=buffer0b618&utm_medium=social&utm_source=facebook.com&utm_campaign=buffer[5/27/2015 4:20:10 AM]

AngularJS Testing Tips: Testing Directives

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><seventhdirective></seventh-directive></div>'))(scope);
scope.$digest();
expect(compiledDirective.fnd('seventhdirective').length).toEqual(0);
});

Testing Transclude
http://www.sitepoint.com/...g-directives/?utm_content=buffer0b618&utm_medium=social&utm_source=facebook.com&utm_campaign=buffer[5/27/2015 4:20:10 AM]

AngularJS Testing Tips: Testing Directives

Transclusion has two cases: transclude set to true and transclude set to an element. I
havent 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.fnd('div[ng-transclude]');
expect(transcludeElem.length).toBe(1);
});
it('should have transclude content', function () {
expect(directiveElem.fnd('p').length).toEqual(1);
});

Conclusion
As youve seen in this article, directives are harder to test when compared with other concepts
in AngularJS. At the same time, they cant be ignored as they control some of the important
http://www.sitepoint.com/...g-directives/?utm_content=buffer0b618&utm_medium=social&utm_source=facebook.com&utm_campaign=buffer[5/27/2015 4:20:10 AM]

AngularJS Testing Tips: Testing Directives

parts of the application. AngularJSs testing ecosystem makes it easier for us to test any piece
of a project. I hope that thanks to this tutorial you are more confdent 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.

Ravi
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 and
particularly on Angular JS. He is an active blogger and a DZone MVB.

You might also like:


6

JavaScript

Learnable

Unit Testing in
AngularJS:
Services,
Controllers &
Providers

Book: Jump Start


JavaScript

by Ravi

by Ara Pehlivanian

Apr 13, 2015

PREMIUM

JavaScript

Why I Love
AngularJS and You
Should Too

by Sandeep Panda

Oct 08, 2014

http://www.sitepoint.com/...g-directives/?utm_content=buffer0b618&utm_medium=social&utm_source=facebook.com&utm_campaign=buffer[5/27/2015 4:20:10 AM]

AngularJS Testing Tips: Testing Directives

Free JavaScript: Novice to Ninja Sample


Get a free 32-page chapter of JavaScript: Novice to Ninja and receive updates on
exclusive offers from SitePoint.

email address
Claim Now
Claim
Now


Comments
Have Your Say

http://www.sitepoint.com/...g-directives/?utm_content=buffer0b618&utm_medium=social&utm_source=facebook.com&utm_campaign=buffer[5/27/2015 4:20:10 AM]

AngularJS Testing Tips: Testing Directives

About

Our Sites

About us

Learnable

Advertise

Reference

Press Room

Web

Legals

Foundations

2000 2015 SitePoint Pty. Ltd.

Connect

Feedback
Write for Us

http://www.sitepoint.com/...g-directives/?utm_content=buffer0b618&utm_medium=social&utm_source=facebook.com&utm_campaign=buffer[5/27/2015 4:20:10 AM]

Vous aimerez peut-être aussi