Testing an AngularJS directive with its template

 

Testing AngularJS directives usually isn’t very hard. Most of the time it is just a matter of instantiating the directive using the $compile() function and interacting with the scope or related controller to verify if the behavior is as expected. However that leaves a bit of a gap as most of the time the interaction between the directives template and it’s scope isn’t tested. With really simple templates you can include them in the template property but using the templateUrl and loading them on demand is much more common, specially with more complex templates. Now when it comes to unit testing the HTTP request to load the template if not doing to work and as a result the interaction isn’t tested. Sure it is possible to use the $httpBackend service to fake the response but that still doesn’t use the actual template so doesn’t really test the interaction.

 

Testing the template

It turns out testing the template isn’t that hard after all, there are just a few pieces to the puzzle. First of all Karma can server up other files beside the normal JavaScript files just fine, so we can tell it to serve our templates as well. With the pattern option for files we can tell Karma to watch and server the templates without including them in the default HTML page loaded. See the files section from the karma.conf.js file below.

   1: files: [

   2:     'app/bower_components/angular/angular.js',

   3:     'app/bower_components/angular-mocks/angular-mocks.js',

   4:     'app/components/**/*.js',

   5:     'app/*.js',

   6:     'tests/*.js',

   7:     {

   8:         pattern: 'app/*.html',

   9:         watched: true,

  10:         included: false,

  11:         served: true

  12:     }

  13: ],

 

With that the files are available on the server. There are two problems here though. First of all when running unit tests the mock $httpBackend is used and that never does an actual HTTP request. Secondly the file is hosted at a slightly different URL, Karma includes ‘/base’ as the root of our files. So just letting AngularJS just load it is out of the question. However if we use a plain XMLHttpRequest object the mock $httpBackend is completely bypassed and we can load what we want. Using the plain XMLHttpRequest object has a second benefit in that we can do a synchronous request instead of the normal asynchronous request and use the response to pre-populate the $templateCache before the unit test runs. Using synchronous HTTP request is not advisable for code on the Internet and should be avoided in any production code but in a unit test like this would work perfectly fine.

So taking an AngularJS directive like this:

   1: angular.module('myApp', [])

   2:     .directive('myDirective', function(){

   3:       return{

   4:         scope:{

   5:           clickMe:'&'

   6:         },

   7:         templateUrl:'/app/myDirective.html'

   8:       }

   9:     });

 

And a template like this:

   1: <button ng-click="clickMe()">Click me</button>

 

Can be easily tested like this:

   1: describe('The myDirective', function () {

   2:     var element, scope;

   3:  

   4:     beforeEach(module('myApp'));

   5:  

   6:     beforeEach(inject(function ($templateCache) {

   7:         var templateUrl = '/app/myDirective.html';

   8:         var asynchronous = false;

   9:         var req = new XMLHttpRequest();

  10:         req.onload = function () {

  11:             $templateCache.put(templateUrl, this.responseText);

  12:         };

  13:         req.open('get', '/base' + templateUrl, asynchronous);

  14:         req.send();

  15:     }));

  16:  

  17:     beforeEach(inject(function ($compile, $rootScope) {

  18:         scope = $rootScope.$new();

  19:         scope.doIt = angular.noop;

  20:  

  21:         var html = '<div my-directive="" click-me="doIt()"></div>'

  22:         element = $compile(html)(scope);

  23:         scope.$apply();

  24:     }));

  25:  

  26:     it('template should react to clicking', function () {

  27:         spyOn(scope, 'doIt');

  28:  

  29:         element.find('button')[0].click();

  30:  

  31:         expect(scope.doIt).toHaveBeenCalled();

  32:     });

  33: });

 

Now making any breaking change to the template, like removing the ng-click, will immediately cause the unit test to fail in Karma.

 

Enjoy!

Leave a Reply

Your email address will not be published. Required fields are marked *