Unit Testing Directives in Angular

I've been following a pattern of Angular development of using components as much as possible, exclusively using controllerAs, and following many recommendations in John Papa's styleguide.

The result is heavy use of directives and privately scoped controllers. This is great for organization, but this leaves the question on how to test what was previously controller code

This article assumes you have karma and angular-mocks already installed and configured.

In this example, I have a simple directive that renders a list of items, it uses another directive that represents each row in the grid (a nested directive).

The project structure looks like this:

/app/environments/list.directive.js
/app/environments/list.html
/app/environments/list-item.directive.js
/app/environments/list-item.html

/app/environments/list.directive.js

My directive declaration is fairly standard. It uses isolate scope with a single value, environments, containing the elements in my list. The directive uses the standard controllerAs format but has a privately scoped controller function. Finally, the directive loads its template from a Url.

The private controller, EnvironmentListController doesn't do much in this case.

(function() {
  'use strict';

  angular
    .module('app.environments')
    .directive('environmentsList', environmentsList);

  function environmentsList() {
    return { 
      restrict: 'E',
      scope: {
        environments: '='        
      },
      templateUrl: '/app/environments/list.html',
      controller: EnvironmentListController,
      controllerAs: 'vm',
      bindToController: true
    };
  }

  EnvironmentListController.$inject = [ ];

  function EnvironmentListController() {
    var vm = this;
    vm.environments = vm.environments;

    activate();

    //////////

    function activate() {
    }
  }

}());

/app/environments/list.html

The template file renders each item in the directive and calls another sub-directive to actually show some additional content.

<ul class="list-group environment-list">  
  <li ng-repeat="environment in vm.environments" class="list-group-item animate-show environment"> 
    <environment-list-item environment="environment"></environemtn-list-item>
  </li>
</ul>  

Template Loading Gotchas

If you noticed the directive declaration, you'll see that we're referencing a template file via templateUrl:

  function environmentsList() {
    return { 
      ...
      templateUrl: '/app/environments/list.html',
      ...
    };
  }

When you run tests, if your directive uses a template file, you will likely get the error:

Error: Unexpected request: GET /app/environments/list.html  

You'll need to set up ngHtml2JsPreprocessor to fix this error. This preprocessor will take HTML files and convert them into JavaScript files. Each file creates a Service that can be injected into your test. This prevents Angular from loading the template. To accomplish this, you'll have to do the following:

Install karma-ng-html2js-preprocessor module
npm install karma-ng-html2js-preprocessor --save-dev  
Add .html files to karma.conf.js
  files: [         
    // js files go here

    // add html files
    'app/**/*.html',     
  ]
Add preprocessor directive to karma.conf.js
    preprocessors: {
      'app/**/*.html': ['ng-html2js']
    },
Add ngHtml2JsPreprocessor config to karma.conf.js
    ngHtml2JsPreprocessor: {
      moduleName: 'templates'
    },

You will need to specify the module name that you will import in your test. I typically use templates as the name of the module.

You may also need to add options for stripPrefix and prependPrefix to this configuration to match your application. For example, in the file system, I often use src/client as the root for my Angular application files. I configure Express to serve these files from a virtiaul directory app. So the file src/client/somefile.html would get served as app/somefile.html.

The preprocessor uses the file system path, not the served path as referenced in our application code. As a result I need to need to strip out the src/client part and prepend /app to match how the application code references the file. My config looks like this:

    ngHtml2JsPreprocessor: {
      moduleName: 'templates',
      stripPrefix: 'src/client',
      prependPrefix: '/app'
    },

Writing the Directive Test

Now you should be all set up to write your test. The first thing we'll do is include our templates.

Add your modules
describe('environment-list Directive', function() {  
  beforeEach(module('templates'));
  beforeEach(module('app.environment'));
});

Here, we add the modules that will be used by our test via angular-mocks. The templates modules is the html2Js output.

Load the Directive via Compiled HTML Code
beforeEach(inject(function($rootScope, $compile) {  
    element = angular.element('<environments-list environments="vm.environments"></environments-list>');
    var scope = $rootScope;
    scope.vm = { 
      environments: environments
    };
    $compile(element)(scope);
    scope.$digest();

    controller = element.controller('environmentsList');    
  }));   

This code will create HTML to execute the directive. It will also grab a reference to the controller via the element.controller method.

Write a test
  describe('#activate', function() {
    it('renders the environments', function() {
      expect(controller.environments).to.be.an('array');
      expect(controller.environments).to.equal(environments);
    });
  });
Mocking nested directives

The last remaining piece of the puzzle is mocking sub-directives. There is a great Stackoverflow discussion on this topic. I think the best solution is to manage sub-directives is to inject a mocked factory for the directive.

You can do this by creating a new factory in the module function. As noted in the above article, include Directive

  beforeEach(module('app.environments', function($provide) {
    $provide.factory('envirionmentListItemDirective', function() { return {}; });
  }));

Simply create a factory for the name of your directive (append the word Directive in the name) of your directive since this is done internally by the compiler.

Full Test Code

Here's the final code for testing a Directive.

describe('environment-list Directive', function() {  
  beforeEach(module('templates'));
  beforeEach(module('app'));
  beforeEach(module('app.environments', function($provide) {
    $provide.factory('environmentListItemDirective', function() { return {}; });
  }));

  var element
    , environments = [ {} ]    
    , controller
    ;

  beforeEach(inject(function($rootScope, $compile) {    
    element = angular.element('<environments-list environments="vm.environments"></environments-list>');
    var scope = $rootScope;
    scope.vm = { 
      environments: environments
    };
    $compile(element)(scope);
    scope.$digest();

    controller = element.controller('environmentsList');    
  }));    

  describe('#activate', function() {
    it('renders the environments', function() {
      expect(controller.environments).to.be.an('array');
      expect(controller.environments).to.equal(environments);
    });
  });

});
comments powered by Disqus