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
- Inject Dependencies:
$compile
(to compile the directive) and$rootScope
(to provide a scope). - Compile the Directive: Use
$compile()
to process the directive with scope. - Trigger Digest Cycle: Call
scope.$digest()
to update bindings. - 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