When writing unit tests for an AngularJS directive, there’s not a breadth of information online past the ‘basics’. This fact came to me rather quickly when I tried to write a simple unit test titled: “it(‘should call the passed function’…”. I had created a directive which accepted an isolate scope expression passed from the parent controller. Side-note: a good recap on isolate scope is found here. I finally figured it out with a little ‘controllerAs’ magic.
To follow along, open this jsfiddle!
“How did this work and what was I testing?”
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
myApp.controller('MyDirectiveController', ['$scope',function($scope){ | |
self.doSomething = function (){ | |
//Call the passed expression | |
$scope.passedExpression(); | |
}; | |
}]); |
This is the entire directive’s controller. My function in the directive’s controller (‘doSomething’) calls the passed expression (‘passedExpression’) from the parent controller. What I was trying to do in my test was verify that when a method in the directive’s controller was called, it was actually calling the function on the parent controller.
The Directive
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
myApp.directive('myDrtv', function () { | |
return { | |
restrict: 'E', | |
scope: { | |
passedVar: '=', | |
passedExpression: '&' | |
}, | |
template: ' | |
<div>Hello {{passedVar}}</div> | |
', | |
controller: 'MyDirectiveController', | |
controllerAs: 'myDirectiveCtrl', | |
replace: false | |
}; | |
}); |
So, our directive accepts the passed expression and it gets assigned to the directive’s isolate scope as “$scope.passedExpression”. My initial thought was to simply test the MyDirectiveController without actually compiling the directive, but that doesn’t give us a full test (and would be way too easy to do!).
“Show Us the Test!”
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
describe('myApp', function () { | |
var element, scope, innerScope, elementCtrl; | |
beforeEach(function () { | |
module('myApp'); | |
//Create Element with our directive | |
element = angular.element('<my-drtv passed-var="passThis" passed-expression="myFunction()">'); | |
inject(function ($rootScope, $compile) { | |
scope = $rootScope.$new(); | |
//Create scope variables to pass to the directive | |
scope.passThis = 'Passing'; | |
scope.myFunction = function(){}; | |
$compile(element)(scope); | |
scope.$digest(); | |
//Now our element is ready and behaving like it would on a page | |
innerScope = element.isolateScope(); | |
elementCtrl = innerScope.myDirectiveCtrl; | |
}); | |
}); | |
it('says hello', function () { | |
expect(element.text()).toBe('Hello Passing'); | |
}); | |
it('should call the passed function', function(){ | |
//Watch our main scope's function | |
spyOn(scope,"myFunction"); | |
expect(scope.myFunction).not.toHaveBeenCalled(); | |
//Tell the element to call it's function that calls the parent's function | |
elementCtrl.doSomething(); | |
expect(scope.myFunction).toHaveBeenCalled(); | |
}); | |
}); |
The first test verifies that our parent scope’s variable was passed to the directive properly. The next one is what caught me. I could not figure out how to call the “doSomething()” function on the element’s controller. I tried using the ‘$controller’ injection to get to it, but the instance it created was not the same as the element’s controller. Once I started debugging, I started looking at the results of the function “element.isolateScope()“. There was a property on that scope object that was returned “myDirectiveCtrl” that I immediately found out was the directive’s controller instance! After that I could call whatever I wanted on it. With jasmine, I’m spying on my parent scope’s function to make sure it is being called.
I did see that there was an “element.controller()” function, but it never worked properly for me. If it works for you, please let me know!
“Now what?”
Go write some better unit tests! Also, write as many directives as you can! I’ve found that moving code out of my templates and into directives has really helped modularize my projects. Being able to reuse a directive is such a time (and code) saver!