Unit Testing AngularJS directive with templateUrl

AngularjsAngularjs DirectiveJasmine

Angularjs Problem Overview


Using AngularJS.

Have a directive.

Directive defines templateUrl.

Directive needs unit testing.

Currently unit testing with Jasmine.

This recommends code like:

describe('module: my.module', function () {
    beforeEach(module('my.module'));

    describe('my-directive directive', function () {
        var scope, $compile;
        beforeEach(inject(function (_$rootScope_, _$compile_, $injector) {
            scope = _$rootScope_;
            $compile = _$compile_;
            $httpBackend = $injector.get('$httpBackend');
            $httpBackend.whenGET('path/to/template.html').passThrough();
        }));

        describe('test', function () {
            var element;
            beforeEach(function () {
                element = $compile(
                    '<my-directive></my-directive>')(scope);
                angular.element(document.body).append(element);
            });

            afterEach(function () {
                element.remove();
            });

            it('test', function () {
                expect(element.html()).toBe('asdf');
            });

        });
    });
});

Running code in Jasmine.

Getting error:

TypeError: Object #<Object> has no method 'passThrough'

templateUrl needs loading as-is

Cannot use respond

May be related to ngMock use rather than ngMockE2E use.

Angularjs Solutions


Solution 1 - Angularjs

You're correct that it's related to ngMock. The ngMock module is automatically loaded for every Angular test, and it initializes the mock $httpBackend to handle any use of the $http service, which includes template fetching. The template system tries to load the template through $http and it becomes an "unexpected request" to the mock.

What you need a way to pre-load the templates into the $templateCache so that they're already available when Angular asks for them, without using $http.

The Preferred Solution: Karma

If you're using Karma to run your tests (and you should be), you can configure it to load the templates for you with the ng-html2js preprocessor. Ng-html2js reads the HTML files you specify and converts them into an Angular module that pre-loads the $templateCache.

Step 1: Enable and configure the preprocessor in your karma.conf.js

// karma.conf.js

preprocessors: {
    "path/to/templates/**/*.html": ["ng-html2js"]
},

ngHtml2JsPreprocessor: {
    // If your build process changes the path to your templates,
    // use stripPrefix and prependPrefix to adjust it.
    stripPrefix: "source/path/to/templates/.*/",
    prependPrefix: "web/path/to/templates/",

    // the name of the Angular module to create
    moduleName: "my.templates"
},

If you are using Yeoman to scaffold your app this config will work

plugins: [ 
  'karma-phantomjs-launcher', 
  'karma-jasmine', 
  'karma-ng-html2js-preprocessor' 
], 

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

ngHtml2JsPreprocessor: { 
  stripPrefix: 'app/', 
  moduleName: 'my.templates' 
},

Step 2: Use the module in your tests

// my-test.js

beforeEach(module("my.templates"));    // load new module containing templates

For a complete example, look at this canonical example from Angular test guru Vojta Jina. It includes an entire setup: karma config, templates, and tests.

A Non-Karma Solution

If you do not use Karma for whatever reason (I had an inflexible build process in legacy app) and are just testing in a browser, I have found that you can get around ngMock's takeover of $httpBackend by using a raw XHR to fetch the template for real and insert it into the $templateCache. This solution is much less flexible, but it gets the job done for now.

// my-test.js

// Make template available to unit tests without Karma
//
// Disclaimer: Not using Karma may result in bad karma.
beforeEach(inject(function($templateCache) {
    var directiveTemplate = null;
    var req = new XMLHttpRequest();
    req.onload = function() {
        directiveTemplate = this.responseText;
    };
    // Note that the relative path may be different from your unit test HTML file.
    // Using `false` as the third parameter to open() makes the operation synchronous.
    // Gentle reminder that boolean parameters are not the best API choice.
    req.open("get", "../../partials/directiveTemplate.html", false);
    req.send();
    $templateCache.put("partials/directiveTemplate.html", directiveTemplate);
}));

Seriously, though. Use Karma. It takes a little work to set up, but it lets you run all your tests, in multiple browsers at once, from the command line. So you can have it as part of your continuous integration system, and/or you can make it a shortcut key from your editor. Much better than alt-tab-refresh-ad-infinitum.

Solution 2 - Angularjs

What I ended up doing was getting the template cache and putting the view in there. I don't have control over not using ngMock, it turns out:

beforeEach(inject(function(_$rootScope_, _$compile_, $templateCache) {
    $scope = _$rootScope_;
    $compile = _$compile_;
    $templateCache.put('path/to/template.html', '<div>Here goes the template</div>');
}));

Solution 3 - Angularjs

This initial problem can be solved by adding this:

beforeEach(angular.mock.module('ngMockE2E'));

That's because it tries to find $httpBackend in ngMock module by default and it's not full.

Solution 4 - Angularjs

The solution I reached needs jasmine-jquery.js and a proxy server.

I followed these steps:

  1. In karma.conf:

add jasmine-jquery.js to your files

files = [
    JASMINE,
    JASMINE_ADAPTER,
    ...,
    jasmine-jquery-1.3.1,
    ...
]

add a proxy server that will server your fixtures

proxies = {
    '/' : 'http://localhost:3502/'
};

2. In your spec

describe('MySpec', function() {
	var $scope, template;
    jasmine.getFixtures().fixturesPath = 'public/partials/'; //custom path so you can serve the real template you use on the app
    beforeEach(function() {
        template = angular.element('<my-directive-name></my-directive-name>');

        module('project');
        inject(function($injector, $controller, $rootScope, $compile, $templateCache) {
            $templateCache.put('partials/resources-list.html', jasmine.getFixtures().getFixtureHtml_('resources-list.html')); //loadFixture function doesn't return a string
       	    $scope = $rootScope.$new();
            $compile(template)($scope);
            $scope.$apply();
        })
    });
});

3. Run a server on your app's root directory

python -m SimpleHTTPServer 3502

4. Run karma.

It took my a while to figure this out, having to search many posts, I think the documentation about this should be clearer, as it is such an important issue.

Solution 5 - Angularjs

My solution:

test/karma-utils.js:

function httpGetSync(filePath) {
  var xhr = new XMLHttpRequest();
  xhr.open("GET", "/base/app/" + filePath, false);
  xhr.send();
  return xhr.responseText;
}

function preloadTemplate(path) {
  return inject(function ($templateCache) {
    var response = httpGetSync(path);
    $templateCache.put(path, response);
  });
}

karma.config.js:

files: [
  //(...)
  'test/karma-utils.js',
  'test/mock/**/*.js',
  'test/spec/**/*.js'
],

the test:

'use strict';
describe('Directive: gowiliEvent', function () {
  // load the directive's module
  beforeEach(module('frontendSrcApp'));
  var element,
    scope;
  beforeEach(preloadTemplate('views/directives/event.html'));
  beforeEach(inject(function ($rootScope) {
    scope = $rootScope.$new();
  }));
  it('should exist', inject(function ($compile) {
    element = angular.element('<event></-event>');
    element = $compile(element)(scope);
    scope.$digest();
    expect(element.html()).toContain('div');
  }));
});

Solution 6 - Angularjs

If you are using Grunt, you can use grunt-angular-templates. It loads your templates in the templateCache and it's tranparent to your specs configuration.

My sample config:

module.exports = function(grunt) {

  grunt.initConfig({
    
    pkg: grunt.file.readJSON('package.json'),

	ngtemplates: {
	    myapp: {
	      options: {
	        base:       'public/partials',
	        prepend:    'partials/',
	        module:     'project'
	      },
	      src:          'public/partials/*.html',
	      dest:         'spec/javascripts/angular/helpers/templates.js'
	    }
	},

	watch: {
	  	templates: {
	    	files: ['public/partials/*.html'],
	    	tasks: ['ngtemplates']
		}
	}

  });

  grunt.loadNpmTasks('grunt-angular-templates');
  grunt.loadNpmTasks('grunt-contrib-watch');

};

Solution 7 - Angularjs

I solved the same problem in a slightly different way than the chosen solution.

  1. First, I installed and configured the ng-html2js plugin for karma. In the karma.conf.js file :

    preprocessors: {
      'path/to/templates/**/*.html': 'ng-html2js'
    },
    ngHtml2JsPreprocessor: {
    // you might need to strip the main directory prefix in the URL request
      stripPrefix: 'path/'
    }
    
  2. Then I loaded the module created in the beforeEach. In your Spec.js file :

    beforeEach(module('myApp', 'to/templates/myTemplate.html'));
    
  3. Then I used $templateCache.get to store it into a variable. In your Spec.js file :

    var element,
        $scope,
        template;
    
    beforeEach(inject(function($rootScope, $compile, $templateCache) {
      $scope = $rootScope.$new();
      element = $compile('<div my-directive></div>')($scope);
      template = $templateCache.get('to/templates/myTemplate.html');
      $scope.$digest();
    }));
    
  4. Finally, I tested it this way. In your Spec.js file:

    describe('element', function() { it('should contain the template', function() { expect(element.html()).toMatch(template); }); });

Solution 8 - Angularjs

To load the template html dynamically into $templateCache you could just use html2js karma pre-processor, as explained here

this boils down to adding templates '.html' to your files in the conf.js file as well preprocessors = { '.html': 'html2js' };

and use

beforeEach(module('..'));

beforeEach(module('...html', '...html'));

into your js testing file

Solution 9 - Angularjs

if you're using Karma, consider using karma-ng-html2js-preprocessor to pre-compile your external HTML templates and avoid having Angular try to HTTP GET them during test execution. I struggled with this for a couple of ours - in my case templateUrl's partial paths resolved during normal app execution but not during tests - due to differences in app vs. test dir structures.

Solution 10 - Angularjs

If you are using the jasmine-maven-plugin together with RequireJS you can use the text plugin to load the template content into a variable and then put it in the template cache.


define(['angular', 'text!path/to/template.html', 'angular-route', 'angular-mocks'], function(ng, directiveTemplate) {
"use strict";



describe('Directive TestSuite', function () {

    beforeEach(inject(function( $templateCache) {
        $templateCache.put("path/to/template.html", directiveTemplate);
    }));

});




});

});

Solution 11 - Angularjs

If you use requirejs in your tests then you can use the 'text' plugin to pull in the html template and put it in the $templateCache.

require(["text!template.html", "module-file"], function (templateHtml){
  describe("Thing", function () {

    var element, scope;

    beforeEach(module('module'));

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

      // VOILA!
      $templateCache.put('/path/to/the/template.html', templateHtml);  

      element = angular.element('<my-thing></my-thing>');
      scope = $rootScope;
      $compile(element)(scope);   

      scope.$digest();
    }));
  });
});

Solution 12 - Angularjs

I resolve this issue with compiling all templates to templatecache. I'm using gulp, you can find similar solution for grunt too. My templateUrls in directives, modals looks like

`templateUrl: '/templates/directives/sidebar/tree.html'`
  1. Add a new npm package in my package.json

    "gulp-angular-templatecache": "1.*"

  2. In gulp file add templatecache and a new task:

    var templateCache = require('gulp-angular-templatecache'); ... ... gulp.task('compileTemplates', function () { gulp.src([ './app/templates/**/*.html' ]).pipe(templateCache('templates.js', { transformUrl: function (url) { return '/templates/' + url; } })) .pipe(gulp.dest('wwwroot/assets/js')); });

  3. Add all js files in index.html

    <script src="/assets/js/lib.js"></script> <script src="/assets/js/app.js"></script> <script src="/assets/js/templates.js"></script>

  4. Enjoy!

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionJesus is LordView Question on Stackoverflow
Solution 1 - AngularjsSleepyMurphView Answer on Stackoverflow
Solution 2 - AngularjsJesus is LordView Answer on Stackoverflow
Solution 3 - AngularjsbullgareView Answer on Stackoverflow
Solution 4 - AngularjsTomas RomeroView Answer on Stackoverflow
Solution 5 - AngularjsbartekView Answer on Stackoverflow
Solution 6 - AngularjsTomas RomeroView Answer on Stackoverflow
Solution 7 - AngularjsglepretreView Answer on Stackoverflow
Solution 8 - AngularjsLiorView Answer on Stackoverflow
Solution 9 - AngularjsNikitaView Answer on Stackoverflow
Solution 10 - AngularjsLeonard BrüningsView Answer on Stackoverflow
Solution 11 - AngularjsTim KindbergView Answer on Stackoverflow
Solution 12 - AngularjskitologView Answer on Stackoverflow