Thursday, June 27, 2013

Angular JS - Unit Testing - Directives

Testing directives is only slightly different than testing controllers or services, as I've covered in previous posts. Since you can inject services into directives, you can still mock those services by providing custom mocks as outlined in my "Unit Testing Services" post. But the major difference in that testing directives involves the DOM, which can make things difficult.

$compile is your friend.


To test a directive, you're going to need to compile a view featuring the directive, then probe DOM elements in that view to assert that they've been affected properly.

Basic Directive Example

This is a simple little directive that basically will change the text of an element to the evaluated value of an expression when the element is clicked.

app.directive('sampleOne', function (){
    // this is an attribute with no required controllers, 
    // and no isolated scope, so we're going to use all the
    // defaults, and just providing a linking function.
    
    return function(scope, elem, attrs) {
      elem.bind('click', function(){
        elem.text(scope.$eval(attrs.sampleOne));
      });
    };
});


Using $compile and angular.element() for testing


So to test this directive, we're going to create a string of html as a view that features the directive we want to test, then we're going to $compile that view against a scope we create. After that, we can alter the scope as we need to, and/or use jqLite (packaged with Angular) or JQuery (works well with Angular) to do some DOM manipulation and test some values.

IMPORTANT: Be sure to call scope.$digest() after you make changes to your scope and before you make your assertions!

Since Angular won't be doing this for you because you're testing, you have to be sure to call $digest() to update your view and model.

describe('Testing sampleOne directive', function() {
  var scope,
      elem,
      directive,
      compiled,
      html;
      
  beforeEach(function (){
    //load the module
    module('plunker');
    
    //set our view html.
    html = '<div sample-one="foo"></div>';
    
    inject(function($compile, $rootScope) {
      //create a scope (you could just use $rootScope, I suppose)
      scope = $rootScope.$new();
      
      //get the jqLite or jQuery element
      elem = angular.element(html);
      
      //compile the element into a function to 
      // process the view.
      compiled = $compile(elem);
      
      //run the compiled view.
      compiled(scope);
      
      //call digest on the scope!
      scope.$digest();
    });
  });

  it('Should set the text of the element to whatever was passed.', function() {
    //set a value (the same one we had in the html)
    scope.foo = 'bar';
    
    //check to see if it's blank first.
    expect(elem.text()).toBe('');
    
    //click the element.
    elem[0].click();
    
    //test to see if it was updated.
    expect(elem.text()).toBe('bar');
  });
});



UPDATE: I've added one more example to the Plunk, so have a look below.


And of course, here's a plunker demonstrating a basic directive test:
http://plnkr.co/edit/oTuRbYTPt8RyybUzk5ND?p=preview

11 comments:

  1. Very useful examples! Easy to understand and to apply, thanks!

    ReplyDelete
  2. Great example, do you have any guidance when using templates. I've seen the solutions for testing with Karma but have yet to see a solution when Chutzpah is being used.

    ReplyDelete
    Replies
    1. With templates it should be as easy as doing a pass through for the template get on $httpBackend. Outside of that maybe I don't understand your question?

      Delete
    2. I've just been trying to figure this out. It looks like the best solution, if you're using Karma, is to precompile the template files using karma-ng-html2js-preprocessor. The README for that plugin neglects to mention two things: One, you have to include the plugin in the "plugins" section of your karma config, and two, in order to make the template available in your tests you have to call module('path-to-template-file.html'). You can see an example of it being used in the ng-directive-testing project on github.

      Delete
    3. So, most of the projects I've been working on predate some of these plugins... so I actually wrote a quick Grunt task that writes a .js file that loads all of the templates in my templates directory into the $templateCache. I'd post the script for you, but it's not my property. :\ But that's one idea, and it's been working well so far.

      Delete
  3. Hello, thanks for all your contributions via this Angular blog! Have you ever had any success with testing nested directives? I can't quite figure how to use some fake or mock directive as the add-on behavior directive as I test my isolate scope/template-based directive. Best I can think of is to overwrite the other directive with a very small/stubbish directive that a test reader could understand quickly.. but its still code which is Bad.

    ReplyDelete
    Replies
    1. Testing nested directives should probably be done the same as testing anything else. You'd unit test your directives' controllers (and linking functions!) and then you'd do a "behavioral" end-to-end test with something like Protractor or Karma to confirm the behavior in the browser.

      Delete
  4. Thanks! This was incredibly helpfull!

    ReplyDelete
  5. Can you provide examples with onFocus? I tried adding that element to DOM but it does not seem to work, readily.

    ReplyDelete
    Replies
    1. It shouldn't be much different. You could also try writing tests for event bindings in Protractor.

      Post a question on StackOverflow. It's pretty hard to figure these things out in the comments on this site.

      Delete

This form allows some basic HTML. It will only create links if you wrap the URL in an anchor tag (Sorry, it's the Blogger default)