48

In some of my directives, I'm adding functions to the scope to handle logic specific for the directive. For example:

link: function(scope, element, attrs) {
         scope.doStuff = function() {
            //do a bunch of stuff I want to test
         }        
      }

How do I go about testing that function? I googled around for how test a directive, but the things I found were more about testing changes on the element. I can certainly compile my directive before each of my tests, but that would wipe out my scope every time. I want to test the function as properties in my scope changes.

Is there any way to get a hold of the object that is returned from the directive definition? Then I could just call the link function directly and test the behavior of each of the functions defined on the scope. Is there a better way to do all this?

I'm using Jasmine to run my tests, and I'm wanting to my scope setup in the describe functions, so I can have multiple it functions for the same scope data.

Community
  • 1
  • 1
dnc253
  • 39,967
  • 41
  • 141
  • 157

5 Answers5

46

Basically, rather than test the link function itself, you'd test the outcome(s) of the directive programmatically. What you would do is write out the directive to a string, and use $compile to have angular process it. Then you test the output to make sure everything is wired up correctly.

Angular's source is full of good examples of how to do this... for example Angular's test of the ngRepeat directive

You can see what they're doing is setting up the directive, Changing the scope (in this case $rootScope) making sure it's $digested, and then testing the DOM it outputs to make sure everything is wired up correctly. You can also test what's in the scope, if the directive is altering that.

The test for ngClick is also pretty interesting, because it shows testing of a browser interaction and it's effect on the scope.

For sake of completeness, here's a snippet from the ngClick tests that I think sums up testing a directive fairly well:

 it('should get called on a click', inject(function($rootScope, $compile) {
   element = $compile('<div ng-click="clicked = true"></div>')($rootScope);
   $rootScope.$digest();
   expect($rootScope.clicked).toBeFalsy();

   browserTrigger(element, 'click');
   expect($rootScope.clicked).toEqual(true);
 }));

So in the case of your scope.doStuff function, I wouldn't test what it's doing, so much as I'd test whatever it's affected on the scope, and it's subsequently effected DOM elements.

taylonr
  • 10,732
  • 5
  • 37
  • 66
Ben Lesh
  • 107,825
  • 47
  • 247
  • 232
  • 2
    my `doStuff` basically just updates different parts of my model. I was hoping to have separate `it`s for the different changes to the model it makes, but that would basically mean re setting up the model each time for each of those `it`s Looking at that ng-repeat example, I think I just need to adjust how I was wanting to test it all, and just have one `it` for the whole method and just adjust the scope as I go along the different `expect`s – dnc253 Nov 16 '12 at 18:32
  • 1
    Nah, you should be able to use a `beforeEach` function to do the setup before each `it`. https://github.com/pivotal/jasmine/wiki/Before-and-After – Ben Lesh Nov 16 '12 at 19:02
  • 1
    I got things working, but it still seems like a lot of overhead to have to compile the directive before every single test, instead of having access to the internal object where I could test without having to compile. – dnc253 Nov 20 '12 at 19:05
  • 1
    Yeah, it's perhaps some overhead... but they're tests, so generally speaking the *test* matters more than the overhead. It's not like your users will be running tests. – Ben Lesh Nov 20 '12 at 19:16
  • @BenLesh what I first `$scope = $rootScope.$new();` and pass it into compile, rather than `$rootScope` – Eugene Nov 27 '14 at 12:32
  • 2
    @Eugene - It shouldn't make any difference. the scope will be "fresh" in each test. – Ben Lesh Nov 30 '14 at 02:04
  • @BenLesh okey. Thought so. Any suggestion on how to test directive, that has required property set? – Eugene Nov 30 '14 at 10:38
  • @Eugene You'd just have your html with the required directive around it. ... if that doesn't answer your question, submit something on SO, because it sounds like you might have a specific issue in mind. – Ben Lesh Dec 03 '14 at 19:01
  • I'm trying to do the same with `
    ` but it doesn't work. Any idea why..?
    – Nimo Nov 10 '15 at 17:08
  • I am curious do we need to do anything different or additional for mocking out $attrs or $el? – Winnemucca Mar 15 '16 at 19:09
2

If needed, it's possible to directly unit test the link method of a directive. See the unit tester of the angular-ice module: "testing a directive configuration"

http://bverbist.github.io/angular-ice/#/unitTester

example usage: https://github.com/bverbist/angular-ice/blob/master/app/components/icebank/bank-account-number-directive_link_test.js

In your case you can keep a reference to the scope object you pass to the directive's link method, and then you can directly test the doStuff function on that scope.

bverbist
  • 21
  • 1
1

I resolved this issue a bit differently.

If you have a very simple link function in your directive and you don't happen to need the 3rd argument(attrs), just get rid of the link function and assign the directive a controller instead.

app.directive('loadIndicator', function() {
  return {
    restrict: 'E',
    replace: true,
    templateUrl: 'blahblah/indicator.html',
    controller: 'LoadIndicatorController'
  };
});

just as you have the args for scope and element in a directive's link function, these 2 args can be injected into an easy-to-test controller as $scope and $element.

If you are capable of creating controllers and unit testing those controllers, then this will be really easy to do.

sqlexception
  • 194
  • 2
  • 3
  • 2
    You should really only use controllers in your directives if you plan on sharing info between two directives. Thus making sort of an API between the two. At least this is according to **Angular Docs: Best Practice: use a controller when you want to expose an API to other directives, otherwise, use a link function** [Found at bottom of page here](https://docs.angularjs.org/guide/directive – britztopher Feb 03 '15 at 17:38
  • 5
    if someone could show me an easy way to unit test the link function, then I'd be happy to follow that practice – sqlexception Feb 04 '15 at 22:58
  • You need to really just need to get the handle on the scope of the directive. I just wrote a blog on this found here [Testing Linking Function](http://brilliantbritz.com/2015/02/04/getting-the-isolate-scope-of-a-directive/) – britztopher Feb 05 '15 at 17:54
  • with the plunker you made here: http://plnkr.co/edit/3P2bGbYNcLajpsLQFp1I?p=preview it makes sense. and it is easy. good to know. thanks – sqlexception Feb 07 '15 at 00:17
0

As stated in a comment reply to @sqlexception. You just really need to get a handle on the directive's scope, which is not hard to do. What you dont want to do is modify your code to satisfy your tests, as it should be the other way around.

To get the scope of the directive just compile like so:

var element = $compile(<directive's html>).($scope)

where $scope is declared with $scope = $rootScrope.$new(). Now we can get the isolate scope by doing element.scope()

I just recently wrote a short blog post about this found here Testing a Linking Function with a plunker to help:

britztopher
  • 1,214
  • 2
  • 16
  • 26
  • 1
    this does not function for me at all. I create an element using $compile('')(scope) with the scope created from $rootScope.$new(); When I access element.scope() it gives me the scope created using new not the isolated scope. And that is kind of what I expect. So am I missing something or should I stop persuing this avenue. – Gurnard Sep 15 '16 at 08:47
  • After some googling it is apparently the isolateScope() that you need to reference: element.isolateScope().... Found it here http://blog.ninja-squad.com/2015/01/27/5-tricks-about-directives-and-tests/ – Gurnard Sep 15 '16 at 09:28
0
 it('should get called on a click', inject(function($rootScope, $compile) {
   var scope = $rootScope.$new();
   element = $compile('<div ng-click="doIt()"></div>')(scope);
   scope.$digest();
   expect(scope.$$childHead.doIt()).toBeDefined();
 }));

Using this $$childHead was the solution for me to the same problem, with this I can cover functions that weren't being called in my tests.