It’s best practice to place the HTML of a directive’s partial within its own file, and reference it in the directive’s definition object via the templateUrl property. However, this causes problems in testing, as most testing frameworks provide no access to the local file. This article shows you how to get around this limitation, and provide any partial file you need in order to test your directive fully.

Tl;dr

To provide the directive’s file to your tests, either:

1. Use the ng-html2js preprocessor

By far the best approach is to use the ng-html2js preprocessor, which will convert a partial’s HTML into a JavaScript string, and make it accessible to your unit tests via AngularJS’s $templateCache.

It performs this feat of magic by reading your partial’s HTML, converting it into a JavaScript string, and placing it into Angular’s $templateCache. It does this within the run() function of a new module it creates for you.

When you declare this module as a dependency within your tests, the run() function is executed and the partial’s HTML is placed within the $templateCache, which the directive then accesses automatically as part of the $compile process.

For example, consider the following simple directive:

(function(){
    'use strict';

    angular
        .module('app')
        .directive('untangledHeader', untangledHeader);

    function untangledHeader(){

        var directive = {
            restrict: 'E',
            templateUrl: 'scripts/app/layout/header/header.tpl.html',
        };

        return directive;
})();

Suppose its partial looks as follows:

// defined in scripts/app/layout/header/header.tpl.html
Header Title

When you run karma, the ng-html2js preprocessor will:

  • create a module with a name you’ve defined in karma.conf.js (in this example, I’ve called it partials);
  • read the partial’s HTML file;
  • convert it into a JavaScript string;
  • place it in $templateCache with the name 'scripts/app/layout/header/header.tpl.html' in the module’s run() function.

For this example, the output of all this will see the partial transformed into the following auto-generated module:

(function(module) {
try {
  module = angular.module('partials');
} catch (e) {
  module = angular.module('partials', []);
}
module.run(['$templateCache', function($templateCache) {
  $templateCache.put('scripts/app/layout/header/header.tpl.html',
     '
Header Title
'); }]); })();

You can now simply include this partial’s module in your tests like so:

beforeEach(module('partials'));

…and the directive will automatically use the JavaScript string that the partials module placed into the $templateCache.

Example test

Putting this altogether, your test will look like this:

describe('Header Directive: untangledHeader', function() {

    beforeEach(module('app'));
    // declare ng-html2js's auto-generated partials module as a dependency
    beforeEach(module('partials'));

    var element,
        scope,
        isolateScope;

    beforeEach(inject(function($rootScope, $compile) {
        scope = $rootScope.$new();
        element = angular.element('');

        // element will enable you to test your directive's element on the DOM
        element = $compile(element)(scope);

        // Digest needs to be called to set any values on the directive's scope
        scope.$digest();

        // If the directive uses isolate scope, we need to get a reference to it 
        // explicitly
        isolateScope = element.isolateScope();
    }));

   it('Should...', function(){
      expect(scope.x).toBe(y);
      expect(isolateScope.a).toBe(b);
   });
});

Pros

The chief advantage of this approach is that you’re actually using the production HTML of the directive that will be used in your application. This is important, as a complex directive may depend on the correct HTML elements and attributes being present. Using a mocked partial will only test the HTML you provide for the test – not the actual HTML used in your app.

A second advantage is the simplicity of this approach once you have it up and running. Simply include the partials module, and you can forget about worrying whether or not you’ve correctly mocked your HTML partials.

Finally, your partial’s HTML is guaranteed never to be out of sync with that of the app. You can make as many changes as you need to your partial’s HTML, without updating the HTML in your tests.

Cons

The (marginal) downside to using the ng-html2js preprocessor is that it doesn’t let you change the partial’s HTML in your unit tests, which you may want to do if you need to test how your directive handles errors (e.g. missing attributes).

It can also be a bit fiddly to setup, but hopefully the following sub-section will help you solve some of the more common issues.

Troubleshooting ng-html2js

Some common errors people (well, me, you might make different ones!) make include:

Not setting the path to your partials correctly

If the ng-html2js preprocessor can’t find the partial’s HTML, it can’t transform it into JavaScript for you.

To check if this is your problem, run the Karma tests in Chrome, and try to find your partials module. By default, it’ll be named the same as the partial’s HTML file, but with .js appended, and will be in the same folder as the existing partial.

For example, a partial called scripts/app/layout/header/header.tpl.html will be replaced with a file called scripts/app/layout/header/header.tpl.html.js (note: the JavaScript version of your partial’s HTML will only replace the original HTML in the browser – the HTML file itself remains unchanged on your filesystem).

If it doesn’t exist, your filepaths are wrong in karma.conf.js.

You can see what files the ng-html2js preprocessor is trying to process by running karma from the command line with the debug log-level switched on:

karma start --log-level debug

This will output the HTML files that ng-html2js has found and is converting. For example, with the header.tpl.html partial used above, you might see the following output:

:DEBUG [preprocessor.html2js]: Processing "/dev/untangled-angular-seed/scripts/app/layout/header/header.tpl.html".

If you don’t see the files you expect, again, your filepaths are wrong in karma.conf.js

Inadvertently changing the partial’s path name that’s placed in the $templateCache

ng-html2s gives you the opportunity to change the name of the partials with the stripPrefix, stripSuffix and appendSuffix configuration options.

However, it’s easy to misuse these. Just remember that the name of your partial that ng-html2js uses must match the value used in the directive’s templateUrl property.

This is because the value used in the directive’s templateUrl property is used by the $compile function to locate the directive’s partial. $compile will use this this value to query $templateCache to see if it’s been loaded into the cache.

If you change the pathname of the partial by incorrectly configuring the ng-html2js preprocessor in karam.conf.js, the partial’s pathname used in $templateCache will no longer match that in the directive’s templateUrl property, and so the $compile function will not be able to find it in the cache.

Here’s an example of a correctly-configured karma.conf.js module.exports = function(config) { config.set({ basePath: ”, frameworks: [‘jasmine’], // Load third-party libraries first, then modules, // then all other components, then // your tests, then your partials’ HTML files: [ ‘dist/lib.min.js’, ‘scripts/app/app.module.js’, ‘scripts/app/*.js’, ‘scripts/app/**/*.module.js’, ‘scripts/app/**/*.js’, ‘scripts/app/**/*.spec.js’, ‘scripts/app/**/*.html’ ], // Tell karma to use ng-html2js preprocessors: { ‘scripts/app/**/*.html’: [‘ng-html2js’] }, // configure ng-html2js ngHtml2JsPreprocessor: { // create a single module that contains templates from all the files moduleName: ‘partials’ } }) };

Useful Links

Pablo Villoslada Puigcerber has a good article on configuring and using ng-html2js, which will help you install and configure this handy preprocessor.

2. Serve a mock partial using $httpBackend

An alternative approach is to use $httpBackend to serve your directive’s partial as a string. This approach makes use of the fact that when the directive is compiled, AngularJS will make an HTTP GET call to retrieve its partial’s HTML file.

In your tests, $httpBackend is used to intercept this call and return a string of your choice. Normally, you do this to check your code with different responses, but in this case, the response you return is actually an HTML string that the $compile function will use as the directive’s partial.

This approach is best suited if you need to change the HTML during testing, or for times when you can’t use the ng-html2js preprocessor.

Note that this approach is not without its flaws: specifically, the HTML returned in your $httpBackend response is not the HTML in your directive, and so you’re testing your mock partial, not the actual partial.

How to do it

Implementing this approach is quite straightforward:

describe('Header Directive: untangledHeader', function() {

    beforeEach(module('app'));

    var element,
        scope,
        isolateScope;

    beforeEach(inject(function($rootScope, $compile, $httpBackend) {

       // scripts/app/layout/header/header.tpl.html is the same path listed in your 
       // directive's templateUrl property
       $httpBackend.whenGET('scripts/app/layout/header/header.tpl.html')
           .respond(‘
your mock partial
'); scope = $rootScope.$new(); element = angular.element('')(scope); mockScope.$digest(); $httpBackend.flush(); // elem now contains your compiled directive as an Angular element. // If the directive defines an isolate scope, and you need to test any // of its properties, you can access the isolate scope as follows: var compiledElementScope = elem.isolateScope(); }); });

Troubleshooting

You need to be careful with this approach, as it’s easy to forget either the mockScope.$digest() call or the $httpBackend.flush(), and without either, your mock partial will not be returned.

In particular, if you get a 'compiledElementScope is undefined' error, this usually means you haven’t called $httpBackend.flush().

You can remedy this by adding the following to your describe() block:

afterEach($httpBackend.verifyNoOutstandingExpectation);

This will throw an exception if you’ve forgotten to flush (just like my Wife – I thank you ;)).

3. Using $templateCache

The final approach is similar to $httpBackend in that it uses an AngularJS service to return a mock partial as a string, rather than serving the actual partial file itself.

In this case, the service used is $templateCache, which Angular will check first when compiling a directive before making the call for the partial’s file on the server. By populating the $templateCache with a mock partial in advance of the directive’s compilation, you can serve the mock partial from the cache.

This again has the benefit that you can serve whatever HTML you want, and even change it during the course of your tests. Equally, it’s quick and simple to setup, as the following shows:

beforeEach(inject(function($templateCache) {
   // When directive is called, return template's html as templateUrl 
   $templateCache.put('/path/to/yourDirectiveTemplate.tpl.html', template);
});

Personally, between this approach and $httpBackend, I prefer the latter, simply because I can have confidence that the partial has definitely been served through the use of the $httpBackend.verifyNoOutstandingExpectation function.

Ultimately though, the approach you really should be using is to setup the ng-html2js preprocessor and test your actual directives with their real partials, not just mocked ones.

Comments
  • Harvey Specter
    Posted at 12:06 am Sep 24, 2016
    marcos
    Reply
    Author

    epic post! finally somebody explain it correctly.

    ty!!

    (The css seem to have disappeared)

  • Harvey Specter
    Posted at 5:18 pm Jan 11, 2017
    SV
    Reply
    Author

    Great article!! If you could please remove background-color: inherit property on .postContent pre tag, it would make the code visible.

  • Leave a Reply to marcos
    Cancel Reply

    You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>