Testing Bound Directives in Angular 1.3+ With Karma and Jasmine Part 1

Spencer Dellis
4 min readJan 9, 2017

--

Back in version 1.3, Angular added a handy property for directives called bindToController. It resolved much of the overhead of using the controllerAs syntax inside a directive with an isolated scope by automatically binding the directive variables to the controller. These changes were great for more complex directives, which wanted the better organization that an isolated scope and a separate controller would bring.

However, using bindToController with a separate controller makes it a little more difficult to set up integration tests. It’s nothing out of this world in terms of complexity, but seeing a good example of how to set up and write your tests in this scenario can definitely save some time. So, without further ado, let’s get to the demo.

Setting the Stage

Say you have an Angular 1.x directive with a specified controller and bindToController set to true:

In this case, we’re describing a directive that, in some way, replaces traditional dropdowns. The parent controller passes in a list of options which can be selected from (options) as well as a variable to store the option that will be selected (currentOption). Nothing crazy here, let’s get to the tests.

A typical approach for testing the directive might be to mock and render the controller and template of the page which uses the directive. Then you can set about triggering various events and making sure the results are correct. Fake a few sequential clicks on the rendered directive and validate that the currentOption is, indeed, what you expect it to be.

If your directive is very simple, this might be enough. However, as the complexity and internal logic of your directive grows, so do your testing responsibilities. Fortunately, there are two great ways to cover the increase in complexity.

The first, which I won’t cover in detail, is to add unit tests for the directive controller. This is a fairly standard practice and you can find plenty of guides and tutorials on how to do it via the magic of google.

The second, is to enhance the integration tests by fetching the controller of your directive and confirming that the internals are also working as you would expect.

This is where the setup of your tests can get a little tricky. I have spent more time than I’d like to admit struggling to properly compile the directive and gain access to the directive’s controller.

So let’s begin by getting the aforementioned setup out of the way.

Preprocessing HTML Templates

Since we’re referencing an external HTML file with the templateUrl attribute on our directive, we’ll need a way to import our HTML file. You can do it manually, or you can save yourself a whole lot of time and use a lovely Karma plugin by the name of ng-html2js. This preprocessor will read all of the HTML files you specify and convert them into an Angular module that takes care of making them available in your tests (it’s fantastic).

Let’s add it to the project package.json :

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

Then, in your karma-conf.js file:

It’s important to strip the path name such that what’s left matches how you reference the file in your directive. Since the value of the directive templateUrl attribute is ‘fancySelect.html’, we need to make sure to strip the path to match that. In this case, the full file path is ‘FancySelect/src/fancySelect.html’ so we use the stripPrefix option to remove the ‘FancySelect/src/’ portion. You can also use the stripSuffix or the prependPrefix attributes to alter the path (visit the plugin’s GitHub repo for details on other configuration options).

Great! Now that we have access to our HTML templates, we can get started on the test file.

Preparing to Write Tests

At the beginning of our top-most describe function we’ll load the directive and the pre-processed template module in a beforeEach statement:

beforeEach(module('fancySelect', 'fancy-templates'));

Then, we create the parent scope for our directive and set the variables that the directive will need from the parent:

var element, controller;beforeEach(inject(function($templateCache, $rootScope, $compile) {
scope= $rootScope.$new();
scope.options = [
{ id: 1, name: 'John', age: 23 },
{ id: 2, name: 'Yoko', age: 34 },
{ id: 3, name: 'Pavel', age: 28 }
]
scope.currentOption = { id: 1, name: 'John', age: 23 };
//more below...

With this, we can build the HTML string representation of our directive:

  //..continued from above  var elementString = 
'<fancy-select
ng-model="currentOption"
options="options">
</fancy-select>';
//more below...

And lastly, we create and store an Angular element using the elementString from the last step, $compile it in the context of the scope, run a $digest cycle to trigger any relevant directive watchers, and fetch and store the directive controller for testing purposes:

  //...continued from above  element = angular.element(elementString);
element = $compile(element)(scope);
scope.$digest();
controller = element.controller('fancySelect');
}));

Phew! The finished result:

Now, there is a problem with the current implementation: the parent scope for every test will be exactly the same. They’ll all have the same initial list of options and the same initial currentOption unless you build a brand new scope and rebuild the directive… all inside an individual test. Hardly optimal.

Fortunately, with a bit of refactoring, we can dodge that issue and add a good deal of flexibility to how we construct our mocked directive.

I’ll be covering that, as well as as how to use our setup to write a variety of actual tests, in Part 2.

Part 2 is now live! You can read it here.

--

--