Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Testing directives that require controllers

So I did see another question: How to mock required directive controller in directive UT which is basically my problem but it seems the answer to this thread was "change your design." I wanted to make sure there is no way to do this. I have a directive that declares a controller which is used by children directives. I am now trying to write jasmine tests for the children directive but I cant get them to compile in the tests because they are dependent on the controller. Here is 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 for me to test the sub directive with jasmine and if so, what am I missing? Even if I could test the directive itself without the controller functions I would be happy.

like image 808
jensengar Avatar asked Oct 07 '13 14:10

jensengar


People also ask

Which directive is used for controller in angular?

AngularJS ng-controller Directive The ng-controller directive adds a controller to your application. In the controller you can write code, and make functions and variables, which will be parts of an object, available inside the current HTML element. In AngularJS this object is called a scope.

What is the difference between angular js directives and controllers?

A controller is usually used to contain and maintain the logic for your view, which gets bound to your view via $scope. A directive is something that you might use repeatedly and is called in your view directly through the directive name which you can pass in as an attribute.

How do you access the directive variable in a controller?

You just create a myVar variable in your controller and pass it to the directive using my-var attribute. Since you are using two way binding, any changes made to myVar by the directive are available in your controller.

What is the difference between controller and link in directives?

Answer:The link option is just a shortcut to setting up a post-link function. controller: The directive controller can be passed to another directive linking/compiling phase. It can be injected into other directices as a mean to use in inter-directive communication.


2 Answers

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.

like image 188
Michael Benford Avatar answered Oct 05 '22 09:10

Michael Benford


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.

like image 25
null Avatar answered Oct 05 '22 10:10

null