Directives are a core part of AngularJS, and testing them ensures they work as expected in different scenarios. Since directives interact with the DOM, their unit tests should focus on behavior, bindings, and rendering.
1. Setting Up the Test Environment
To test directives, we use Jasmine, Karma, and ngMock.
Example: Loading Required Modules in Tests
beforeEach(module('app')); // Load the application module
beforeEach(module('templates')); // If using templates (HTML files)
2. Creating a Simple Directive
Let’s create a simple directive that displays a custom message.
Directive: customMessage
app.directive('customMessage', function() {
return {
restrict: 'E',
scope: {
message: '@'
},
template: '<p>{{ message }}</p>'
};
});
3. Unit Testing the Directive
We use $compile
to create the directive element and $rootScope
to manage scope.
Unit Test for Rendering Message
describe('Directive: customMessage', function() {
var $compile, $rootScope;
beforeEach(module('app'));
beforeEach(inject(function(_$compile_, _$rootScope_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
}));
it('should display the correct message', function() {
var scope = $rootScope.$new();
scope.testMessage = "Hello, AngularJS!";
var element = $compile('<custom-message message="{{testMessage}}"></custom-message>')(scope);
scope.$digest(); // Trigger digest cycle
expect(element.text().trim()).toBe("Hello, AngularJS!");
});
});
Breakdown:
✔ Creates a new scope using $rootScope.$new()
.
✔ Compiles the directive with $compile
.
✔ Updates scope variables and triggers $digest()
.
✔ Checks the rendered output.
4. Testing Directive with Isolated Scope (Two-Way Binding)
Directive with Two-Way Binding (=
)
app.directive('counter', function() {
return {
restrict: 'E',
scope: {
count: '='
},
template: '<button ng-click="count = count + 1">Count: {{ count }}</button>'
};
});
Unit Test for Two-Way Binding
describe('Directive: counter', function() {
var $compile, $rootScope;
beforeEach(module('app'));
beforeEach(inject(function(_$compile_, _$rootScope_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
}));
it('should increment count when button is clicked', function() {
var scope = $rootScope.$new();
scope.counterValue = 5;
var element = $compile('<counter count="counterValue"></counter>')(scope);
scope.$digest();
// Simulate button click
element.find('button').triggerHandler('click');
expect(scope.counterValue).toBe(6);
});
});
✔ Simulates user interaction using triggerHandler('click')
.
✔ Ensures two-way binding updates the scope value.
5. Testing Directives with ngModel
(Forms & Input Fields)
Directive for Custom Input Validation
app.directive('validNumber', function() {
return {
require: 'ngModel',
link: function(scope, element, attrs, ngModel) {
ngModel.$validators.validNumber = function(value) {
return /^[0-9]+$/.test(value);
};
}
};
});
Unit Test for Form Validation
describe('Directive: validNumber', function() {
var $compile, $rootScope;
beforeEach(module('app'));
beforeEach(inject(function(_$compile_, _$rootScope_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
}));
it('should validate numeric input correctly', function() {
var scope = $rootScope.$new();
var element = $compile('<input type="text" ng-model="inputValue" valid-number>')(scope);
scope.$digest();
var ngModelCtrl = element.controller('ngModel');
// Test valid number
ngModelCtrl.$setViewValue('123');
scope.$digest();
expect(ngModelCtrl.$valid).toBe(true);
// Test invalid input
ngModelCtrl.$setViewValue('abc');
scope.$digest();
expect(ngModelCtrl.$valid).toBe(false);
});
});
✔ Uses ngModel.$setViewValue('123')
to simulate user input.
✔ Checks $valid
state of the input field.
6. Testing Directives with External Templates
Directive Using an External Template
app.directive('userCard', function() {
return {
restrict: 'E',
scope: {
user: '='
},
templateUrl: 'user-card.html'
};
});
Test Case for External Template
To test directives with external templates, use $templateCache
to preload the template.
beforeEach(module('app'));
beforeEach(module('templates/user-card.html')); // Preload template
it('should display user name in card', function() {
var scope = $rootScope.$new();
scope.user = { name: 'John Doe' };
var element = $compile('<user-card user="user"></user-card>')(scope);
scope.$digest();
expect(element.text()).toContain('John Doe');
});
✔ Loads external templates using $templateCache
.
✔ Ensures template rendering works correctly.
7. Testing Directives with Event Handling ($emit
, $broadcast
, $on
)
Directive that Emits an Event
app.directive('notifyClick', function() {
return {
restrict: 'A',
link: function(scope, element) {
element.on('click', function() {
scope.$emit('customEvent', 'Clicked!');
});
}
};
});
Unit Test for Event Emission
it('should emit customEvent on click', function() {
var scope = $rootScope.$new();
spyOn(scope, '$emit');
var element = $compile('<div notify-click></div>')(scope);
scope.$digest();
element.triggerHandler('click');
expect(scope.$emit).toHaveBeenCalledWith('customEvent', 'Clicked!');
});
✔ Uses spyOn(scope, '$emit')
to check if the event was emitted.
✔ Simulates clicks using triggerHandler('click')
.
Summary of Best Practices for Testing AngularJS Directives
Best Practice | Why It’s Important |
---|---|
Use $compile and $rootScope | Helps create directive instances dynamically. |
Use $digest() | Ensures data binding updates the DOM. |
Mock external templates | Avoids unnecessary network calls in tests. |
Use triggerHandler() for DOM events | Simulates user interactions. |
Use $templateCache for template-based directives | Prevents dependency issues. |
Use $setViewValue() for ngModel testing | Simulates form input validation. |
Avoid DOM manipulation in tests | Test behavior, not implementation. |