How to test custom directives?

Loading

Custom directives are a core feature in AngularJS that allow developers to create reusable UI components. When writing unit tests for directives, we need to ensure that:

  • The directive compiles correctly.
  • It manipulates the DOM as expected.
  • It interacts properly with the scope.
  • It responds to user interactions correctly.

To test AngularJS directives, we use Karma as the test runner and Jasmine for writing test cases.


1. Setting Up the Test Environment

Before testing, ensure that you have Karma, Jasmine, and Angular Mocks installed:

npm install karma karma-jasmine jasmine-core angular-mocks --save-dev

Include angular-mocks.js in karma.conf.js:

files: [
'node_modules/angular/angular.js',
'node_modules/angular-mocks/angular-mocks.js',
'app.js',
'directives/*.js',
'tests/**/*.spec.js'
]

2. Example Custom Directive

Let’s create a simple directive called myDirective that displays a message.

Directive (myDirective.js)

app.directive('myDirective', function() {
return {
restrict: 'E',
template: '<div class="message">{{ text }}</div>',
scope: {
text: '@'
}
};
});

Directive Usage in HTML

<my-directive text="Hello, AngularJS!"></my-directive>

3. Testing the Directive

We will now write a test to ensure that:

  • The directive compiles correctly.
  • The text attribute is properly bound.

Test File (myDirective.spec.js)

describe('myDirective', function() {
var $compile, $rootScope;

beforeEach(module('myApp'));

beforeEach(inject(function(_$compile_, _$rootScope_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
}));

it('should render the correct text', function() {
var scope = $rootScope.$new();
var element = $compile('<my-directive text="Hello, AngularJS!"></my-directive>')(scope);
scope.$digest();

expect(element.find('div').text()).toBe('Hello, AngularJS!');
});
});

Explanation

  1. Inject Dependencies: $compile (to compile the directive) and $rootScope (to provide a scope).
  2. Compile the Directive: Use $compile() to process the directive with scope.
  3. Trigger Digest Cycle: Call scope.$digest() to update bindings.
  4. Check Rendered Text: element.find('div').text() ensures the directive correctly binds and displays text.

4. Testing an Interactive Directive

Let’s create another directive with event handling.

Directive (clickDirective.js)

app.directive('clickDirective', function() {
return {
restrict: 'E',
template: '<button ng-click="count = count + 1">Click me</button>',
scope: {},
link: function(scope) {
scope.count = 0;
}
};
});

Test File (clickDirective.spec.js)

describe('clickDirective', function() {
var $compile, $rootScope;

beforeEach(module('myApp'));

beforeEach(inject(function(_$compile_, _$rootScope_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
}));

it('should increase count on button click', function() {
var scope = $rootScope.$new();
var element = $compile('<click-directive></click-directive>')(scope);
scope.$digest();

var button = element.find('button');
expect(scope.count).toBe(0);

button.triggerHandler('click');
expect(scope.count).toBe(1);
});
});

Key Takeaways

triggerHandler('click') simulates a button click.
Ensures scope updates correctly after user interaction.


5. Testing a Directive with transclude

Directive with transclude (transcludeDirective.js)

app.directive('transcludeDirective', function() {
return {
restrict: 'E',
transclude: true,
template: '<div class="wrapper"><ng-transclude></ng-transclude></div>'
};
});

Test File (transcludeDirective.spec.js)

describe('transcludeDirective', function() {
var $compile, $rootScope;

beforeEach(module('myApp'));

beforeEach(inject(function(_$compile_, _$rootScope_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
}));

it('should transclude content correctly', function() {
var scope = $rootScope.$new();
var element = $compile('<transclude-directive>Transcluded Content</transclude-directive>')(scope);
scope.$digest();

expect(element.find('.wrapper').text()).toBe('Transcluded Content');
});
});

Key Takeaways

✔ Ensures that transcluded content appears inside the directive.
✔ Uses element.find('.wrapper').text() to verify the output.


6. Testing Directives with Isolated Scope

Directive with Isolated Scope (isolatedScopeDirective.js)

app.directive('isolatedDirective', function() {
return {
restrict: 'E',
template: '<div>{{ name }}</div>',
scope: {
name: '@'
}
};
});

Test File (isolatedScopeDirective.spec.js)

describe('isolatedDirective', function() {
var $compile, $rootScope;

beforeEach(module('myApp'));

beforeEach(inject(function(_$compile_, _$rootScope_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
}));

it('should bind scope variables correctly', function() {
var scope = $rootScope.$new();
scope.username = 'Narendra';
var element = $compile('<isolated-directive name="{{username}}"></isolated-directive>')(scope);
scope.$digest();

expect(element.text()).toBe('Narendra');
});
});

✔ Uses attribute binding (@) to pass data from parent to directive.
Ensures scope binding updates correctly.


7. Best Practices for Testing Directives

Use $compile() to instantiate the directive dynamically.
Call scope.$digest() after compilation to trigger AngularJS bindings.
Use element.find() to select elements inside the directive.
Use triggerHandler() to simulate user actions like clicks or keypresses.
Mock services if the directive depends on an API call using $provide.


8. Running the Tests

Run the tests using Karma:

karma start

For continuous testing:

karma start --auto-watch

Leave a Reply

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