Testing directives that require controllers
Solution 1:
I can think of two approaches:
1) Use both directives
Let's assume we have the following directives:
app.directive('foo', function() {
return {
restrict: 'E',
controller: function($scope) {
this.add = function(x, y) {
return x + y;
}
}
};
});
app.directive('bar', function() {
return {
restrict: 'E',
require: '^foo',
link: function(scope, element, attrs, foo) {
scope.callFoo = function(x, y) {
scope.sum = foo.add(x, y);
}
}
};
});
In order to test the callFoo
method, you can simply compile both directives and let bar
use foo
's implementation:
it('ensures callFoo does whatever it is supposed to', function() {
// Arrange
var element = $compile('<foo><bar></bar></foo>')($scope);
var barScope = element.find('bar').scope();
// Act
barScope.callFoo(1, 2);
// Assert
expect(barScope.sum).toBe(3);
});
Working Plunker.
2) Mock foo's controller out
This one is not quite straightforward and a little tricky. You could use element.controller()
to get the controller of an element, and mock it out with Jasmine:
it('ensures callFoo does whatever it is supposed to', function() {
// Arrange
var element = $compile('<foo><bar></bar></foo>')($scope);
var fooController = element.controller('foo');
var barScope = element.find('bar').scope();
spyOn(fooController, 'add').andReturn(3);
// Act
barScope.callFoo(1, 2);
// Assert
expect(barScope.sum).toBe(3);
expect(fooController.add).toHaveBeenCalledWith(1, 2);
});
Working Plunker.
The tricky part comes up when one directive uses the other's controller right away in its link
function:
app.directive('bar', function() {
return {
restrict: 'E',
require: '^foo',
link: function(scope, element, attrs, foo) {
scope.sum = foo.add(parseInt(attrs.x), parseInt(attrs.y));
}
};
});
In this case you need to compile each directive individually so you can mock the first one out before the second one uses it:
it('ensures callFoo does whatever it is supposed to', function() {
// Arrange
var fooElement = $compile('<foo></foo>')($scope);
var fooController = fooElement.controller('foo');
spyOn(fooController, 'add').andReturn(3);
var barElement = angular.element('<bar x="1" y="2"></bar>')
fooElement.append(barElement);
// Act
barElement = $compile(barElement)($scope);
var barScope = barElement.scope();
// Assert
expect(barScope.sum).toBe(3);
expect(fooController.add).toHaveBeenCalledWith(1, 2);
});
Working Plunker.
The first approach is way easier than the second one, but it relies on the implementation of the first directive, i.e, you're not unit testing things. On the other hand, although mocking the directive's controller isn't so easy, it gives you more control over the test and removes the dependency on the first directive. So, choose wisely. :)
Finally, I'm not aware of an easier way to do all of the above. If anyone knows of a better approach, please improve my answer.
Solution 2:
Forking on the (fantastic) answer of Michael Benford.
If you want to completely isolate your controller/directive in your test, you'll need a slightly different approach.
3) Mocking any required parent controller completely
When you associate a controller with a directive, an instance of the controller gets stored in the data store of the element. The naming convention for the key value is '$' + name of directive + 'Controller'. Whenever Angular tries to resolve a required controller, it traverse the data hierarchy using this convention to locate the required controller. This can easily be manipulated by inserting mocked controller instances into parent elements:
it('ensures callFoo does whatever it is supposed to', function() {
// Arrange
var fooCtrl = {
add: function() { return 123; }
};
spyOn(fooCtrl, 'add').andCallThrough();
var element = angular.element('<div><bar></bar></div>');
element.data('$fooController', fooCtrl);
$compile(element)($scope);
var barScope = element.find('bar').scope();
// Act
barScope.callFoo(1, 2);
// Assert
expect(barScope.sum).toBe(123);
expect(fooCtrl.add).toHaveBeenCalled();
});
Working Plunker.
4) Separating link method
The best approach, in my opinion, is by isolating the link method. All the previous approaches actually test too much and, when situations get a little bit more complex than the simple examples provided here, they require too much of a setup.
Angular has the perfect support for this separation of concern:
// Register link function
app.factory('barLinkFn', function() {
return function(scope, element, attrs, foo) {
scope.callFoo = function(x, y) {
scope.sum = foo.add(x, y);
};
};
});
// Register directive
app.directive('bar', function(barLinkFn) {
return {
restrict: 'E',
require: '^foo',
link: barLinkFn
};
});
And by changing our beforeEach to include our link function ... :
inject(function(_barLinkFn_) {
barLinkFn = _barLinkFn_;
});
... we can do:
it('ensures callFoo does whatever it is supposed to', function() {
// Arrange
var fooCtrl = {
add: function() { return 321; }
};
spyOn(fooCtrl, 'add').andCallThrough();
barLinkFn($scope, $element, $attrs, fooCtrl);
// Act
$scope.callFoo(1, 2);
// Assert
expect($scope.sum).toBe(321);
expect(fooCtrl.add).toHaveBeenCalled();
});
Working Plunker.
This way we're only testing the things that are concerned and the same approach can be used to isolate the compile function if needed.