Ad-Hockery

ad-hockery: /ad·hok'@r·ee/, n.
Gratuitous assumptions... which lead to the appearance of semi-intelligent behavior but are in fact entirely arbitrary. Jargon File

Unit testing Angular directives that use controller and templateUrl

I spent an hour or so this morning figuring out how to unit test an Angular directive that uses a controller and a template loaded from a file (as opposed to inline). There’s a useful example in the ng-directive-testing repository but I thought a quick summary would be useful (as much to remind me the next time I have to do it as anything). Also the examples there test by interacting with DOM elements rather than directly with the directive’s scope.

I’m using the Yeoman Angular generator so my tests are using Jasmine & running with Karma.

In order to get my directive’s template to load I used the karma-ng-html2js-preprocessor plugin for Karma:

npm install karma-ng-html2js-preprocessor --save-dev

This plugin compiles HTML templates into JavaScript (I guess a little like the way the Handlebars compiler works) which are then exposed as an Angular module that your tests can depend on.

I was previously using grunt-karma 0.4.3 which I guess was what the Yeoman generator set me up with. In order to get the plugin to work I had to upgrade to 0.6.1 which also meant reconfiguring my karma.conf.js to the new style where the config is exported as a module. Karma was pretty good about pointing out what was wrong in my existing config so it was easy enough to update.

Then I added the specific config for the plugin.

preprocessors: {
  '**/*.coffee': 'coffee',
  'app/views/*.html': 'ng-html2js'
},

ngHtml2JsPreprocessor: {
  stripPrefix: 'app/',
},

The CoffeeScript pre-processor is the default for Karma but I found I had to declare it explicitly when declaring another preprocessor. The ng-html2js preprocessor operates on all HTML files under app/views which is where the Yeoman generator places them. The other bit of config under ngHtml2JsPreprocessor determines how the file path is converted to a module name the test can load. I’m removing the app/ folder so that the module name will be the same as the path to the file declared in my directive’s templateUrl property.

In order to load the template module I just added the template path to the Jasmine test:

beforeEach module 'myApp', 'views/my-template.html'

My directive uses a controller that attaches a function to the scope. I wanted to write a test where I call the function directly on the scope as I would in a regular controller unit test. I found two problems with this. Firstly, the controller was not executing and secondly the scope that I compile the directive against is the parent of the scope the controller interacts with not the same one.

To fix the first problem I explicitly called $digest() on the parent scope. That causes the controller function to execute. Then I could grab the directive’s scope using the DOM element’s data method.

scope = {}

beforeEach inject ($compile, $rootScope) ->
    # the compilation step generated by Yeoman
    element = angular.element '<my-directive/>'
    element = $compile(element) $rootScope

    # execute the controller function
    $rootScope.$digest()

    # the directive's scope is a child of the one used to compile the element
    scope = element.data '$scope'

When tests subsequently use that scope variable they can directly call the scope functions declared by the controller.

Web Statistics