Testing directives in AngularJS

Loading

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 PracticeWhy It’s Important
Use $compile and $rootScopeHelps create directive instances dynamically.
Use $digest()Ensures data binding updates the DOM.
Mock external templatesAvoids unnecessary network calls in tests.
Use triggerHandler() for DOM eventsSimulates user interactions.
Use $templateCache for template-based directivesPrevents dependency issues.
Use $setViewValue() for ngModel testingSimulates form input validation.
Avoid DOM manipulation in testsTest behavior, not implementation.

Leave a Reply

Your email address will not be published. Required fields are marked *