Académique Documents
Professionnel Documents
Culture Documents
MENU
JavaScript
Tweet
Subscribe
Ravi
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.
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:
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.
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:
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:
expect(spanElement).toBeDefned();
expect(spanElement.text()).toEqual(scope.text);
});
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:
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.
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]
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'
};
});
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:
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.
angular.module('sampleDirectives').directive('ffthDirective',
function () {
return {
scope:{
confg: '=',
notify: '@',
onChange:'&'
}
}
};
})
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.
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
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"
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]
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]
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.
JavaScript
Learnable
Unit Testing in
AngularJS:
Services,
Controllers &
Providers
by Ravi
by Ara Pehlivanian
PREMIUM
JavaScript
Why I Love
AngularJS and You
Should Too
by Sandeep Panda
email address
Claim Now
Claim
Now
Comments
Have Your Say
About
Our Sites
About us
Learnable
Advertise
Reference
Press Room
Web
Legals
Foundations
Connect
Feedback
Write for Us