Testing directives requiring controllers

So, I once again asked the question: How to mock the required directory controller in the UT directive , which is basically my problem, but it seems that the answer to this question was "changed your design." I wanted to make sure this was not possible. I have a directive that declares a controller that is used by children directives. Now I'm trying to write jasmine tests for the children directive, but I can not compile them in the tests, because they depend on the controller. Here's what it looks like:

addressModule.directive('address', ['$http', function($http){ return { replace: false, restrict: 'A', scope: { config: '=' }, template: '<div id="addressContainer">' + '<div ng-if="!showAddressSelectionPage" basic-address config="config"/>' + '<div ng-if="showAddressSelectionPage" address-selector addresses="standardizedAddresses"/>' + '</div>', controller: function($scope) { this.showAddressInput = function(){ $scope.showAddressSelectionPage = false; }; this.showAddressSelection = function(){ $scope.getStandardizedAddresses(); }; this.finish = function(){ $scope.finishAddress(); }; }, link: function(scope, element, attrs) { ... } } }]) 

child directive:

 addressModule.directive('basicAddress360', ['translationService', function(translationService){ return { replace: true, restrict: 'A', scope: { config: '=' }, template: '...', require: "^address360", link: function(scope, element, attrs, addressController){ ... } } }]) 

jasmine test:

 it("should do something", inject(function($compile, $rootScope){ parentHtml = '<div address/>'; subDirectiveHtml = '<div basic-address>'; parentElement = $compile(parentHtml)(rootScope); parentScope = parentElement.scope(); directiveElement = $compile(subDirectiveHtml)(parentScope); directiveScope = directiveElement.scope(); $rootScope.$digest(); })); 

Is there no way to check the sub-directive with jasmine, and if so, what am I missing? Even if I could check the directive itself without the controller functions, I would be happy.

+43
angularjs jasmine
Oct 07 '13 at 14:08
source share
3 answers

I can imagine two approaches:

1) Use both directives

Suppose 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); } } }; }); 

To test the callFoo method, you can simply compile both directives and let bar use the foo 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); }); 

Worker Plunker .

2) mock foo controller

It is not quite simple and a bit complicated. You can use element.controller() to get the element's controller, and mock it 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); }); 

Worker Plunker .

The tricky part is when one directive immediately uses another controller 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 that you can make fun of the first one before the second 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); }); 

Worker Plunker .

The first approach is simpler than the second, but it is based on the implementation of the first directive, i.e. you are not checking objects. On the other hand, while mocking the directory controller is not so easy, it gives you more control over the test and eliminates the dependency on the first directive. So choose wisely. :)

Finally, I don’t know an easier way to do all this. If anyone knows of a better approach, please improve my answer.

+69
Oct 07 '13 at 19:11
source share
β€” -

Passion for the (fantastic) Michael Benford answer.

If you want to completely isolate your controller / directive in your test, you will need a slightly different approach.

3) Completely mocks any necessary parent controller

When you associate the controller with a directive, the controller instance is stored in the item's data store. The naming convention for the key value is "$" + the name of the directive + "Controller". Whenever Angular tries to resolve the required controller, it traverses the data hierarchy using this convention to find the required controller. This can be easily manipulated by inserting the original controller instances into it:

 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(); }); 

Worker Plunker.

4) Separation reference method

The best approach, in my opinion, is to isolate the link method. All of the previous approaches are actually too complicated, and when the situations become a little more complicated than the simple examples given here, they require too much customization.

Angular has excellent 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:

 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.

In this way, we check only those things that can be used to isolate the compilation function if necessary.

+53
Nov 13 '13 at 10:20
source share

5) Implementation of the definition of directives and mockery of the function of the controller

Another approach is to introduce a definition of a directive and make fun of everything we need. The best thing about this is that you can write complete unit tests for your children directive, depending on your parents.

Using the inject () command, you can enter any definition of directives by specifying the name of the directive + 'Directive', and then access its methods and replace them as necessary

 it('ensures callFoo does whatever it is supposed to', inject(function(fooDirective) { var fooDirectiveDefinition = fooDirective[0]; // Remove any behavior attached to original link function because unit // tests should isolate from other components fooDirectiveDefinition.link = angular.noop; // Create a spy for foo.add function var fooAddMock = jasmine.createSpy('add'); // And replace the original controller with the new one defining the spy fooDirectiveDefinition.controller = function() { this.add = fooAddMock; }; // Arrange var element = $compile('<foo><bar></bar></foo>')($scope); var barScope = element.find('bar').scope(); // Act barScope.callFoo(1, 2); // Verify that add mock was called with proper parameters expect(fooAddMock).toHaveBeenCalledWith(1, 2); })); 

The idea was proposed by Daniel Tabuenca at AngularJS Google Group.

In this Plunker, Daniel mocks the ngModel directive

+9
Apr 14 '14 at 15:39
source share



All Articles