123

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.

Sreeram Nair
  • 2,369
  • 12
  • 27
Jesus is Lord
  • 14,971
  • 11
  • 66
  • 97
  • 1
    I haven't used `.passThrough();` in that way, but from the docs, have you tried something like: `$httpBackend.expectGET('path/to/template.html'); // do action here $httpBackend.flush();` I think this fits your usage better - you're not wanting to catch the request, i.e. `whenGet()`, but instead check it is sent, and then actually send it? – Alex Osborn Mar 05 '13 at 02:56
  • 1
    Thanks for the reply. I don't think that `expectGET` sends requests...at least out of the box. In the [docs](http://docs.angularjs.org/api/ngMock.$httpBackend) their example with `/auth.py` has a `$httpBackend.when` prior to the `$httpBackend.expectGET` and `$httpBackend.flush` calls. – Jesus is Lord Mar 05 '13 at 03:09
  • 2
    That's correct, `expectGet` is just checking whether a request was attempted. – Alex Osborn Mar 05 '13 at 03:13
  • 1
    Ah. Well I need a way to tell the `$httpBackend` mock to actually use the URL provided in the directive under `templateUrl` and go get it. I thought `passThrough` would do this. Do you know of a different way to do this? – Jesus is Lord Mar 05 '13 at 03:15
  • 2
    Hmm, I haven't done much e2e testing yet, but checking the docs - have you tried using the e2e backend instead - I think that's why you got no method passThrough - http://docs.angularjs.org/api/ngMockE2E.$httpBackend – Alex Osborn Mar 05 '13 at 03:22
  • 1
    I just took a quick look at the docs and there doesn't seem to be mention about `.expectGET(...).passThrough();` in testing section. It appears as through they have replaced that functionality with asserts like `.expectGET(...).respond(,);` (http://docs.angularjs.org/api/ngMock.$httpBackend) – brysgo Mar 05 '13 at 03:26
  • 1
    @AlexOsborn is right if you wanted to do end to end testing you should definetely use ngMockE2E. – brysgo Mar 05 '13 at 03:29

12 Answers12

186

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.

SleepyMurph
  • 2,079
  • 2
  • 14
  • 9
  • 6
    This may be obvious, but if others get stuck on the same thing and look here for answers: I couldn't get it to work without also adding the `preprocessors` file pattern (e.g. `"path/to/templates/**/*.html"`) to the `files` section in `karma.conf.js`. – Johan Jun 18 '14 at 17:27
  • 1
    So are there any major issues with not waiting for the response before continuing? Will it just update the value when the request comes back (I.E. takes 30 seconds)? – Jackie Aug 11 '14 at 13:09
  • 1
    @Jackie I assume you're talking about the "non-Karma" example where I use the `false` parameter for the XHR's `open` call to make it synchronous. If you don't do that, the execution will merrily continue and start executing your tests, without having the template loaded. That gets your right back to the same problem: 1) Request for template goes out. 2) Test starts executing. 3) The test compiles a directive, and the template is still not loaded. 4) Angular requests the template through its `$http` service, which is mocked out. 5) The mock `$http` service complains: "unexpected request". – SleepyMurph Aug 30 '14 at 01:58
  • 1
    I was able to run grunt-jasmine without Karma. – FlavorScape Sep 02 '14 at 00:04
  • 5
    Another thing: you need to install karma-ng-html2js-preprocessor (`npm install --save-dev karma-ng-html2js-preprocessor`), and add it to the plugins section of your `karma.conf.js`, according to http://stackoverflow.com/a/19077966/859631. – Vincent Sep 30 '14 at 13:51
  • 1
    If you are using yeoman angular generator and karma, here is the config that worked for me `// Which plugins to enable plugins: [ 'karma-phantomjs-launcher', 'karma-jasmine', 'karma-ng-html2js-preprocessor' ], preprocessors: { 'app/views/*.html': ['ng-html2js'] }, ngHtml2JsPreprocessor: { stripPrefix: 'app/', moduleName: 'templates' },` – Paul Sheldrake Oct 05 '14 at 23:53
  • 1
    I'm unittesting a directive with a template using this method (karma + jasmine + ngHtml2JsPreprocessor) . The only problem is that the ng-if's are not compiled (even after scope.$digest()). Do you know how I can get the fully compiled html? – Vincent Jan 14 '15 at 09:59
  • @Vincent ngHtml2Js doesn't activate any directives or anything. It just converts the HTML to JS strings. So the `ng-if` attributes in your HTML should surely be present in the Angular template. Is the problem that they're not taking effect? If so, double-check the "if" expression to make sure it really has the value you expect. Also, keep in mind that unlike `ng-show`, `ng-if` actually removes the elements from the DOM when the expression is false, and recreates them when it's true. So if the elements are missing from the DOM, that could just mean it's working. – SleepyMurph Jan 14 '15 at 17:37
  • Have to down-vote it because the first karma solution does not work for me. the non-karma solution works but it costs me 2h to realize the first work does not work. @bartek's solution is much better than the non-karma solution in this answer. – chfw Feb 23 '15 at 12:14
  • Remember to do scope.$digest() to load the templates. – Roar Skullestad Mar 04 '15 at 19:25
  • after searching for about 20 minutes came across this solution ... went to start configuring my karma.conf file but since I'm using yeoman (https://github.com/Swiip/generator-gulp-angular) this preprocessor was already installed / configured ... all i had to do was add beforeEach(module("my.templates")); ... substituting my.templates and i was done! thanks! – markS Jul 27 '15 at 13:16
  • @chfw Did you call `$scope.$digest()` after the call to `$compile`? I just spent probably the same amount of time trying to figure out why the template HTML wasn't loading until I realized I was missing that. – Rahul Patel Feb 17 '16 at 00:12
  • I got ) ReferenceError: XMLHttpRequest is not defined ReferenceError: XMLHttpRequest is not defined. Not sure how others got this to work. – Winnemucca Mar 03 '16 at 00:33
  • @stevek That is very strange. XMLHttpRequest is a very old piece of browser API that should be present in all modern (and not so modern) browsers. If you are running that test code on a page in a browser, XMLHttpRequest should be defined. The simplest explanation would be a typo. Are you sure it's spelled and capitalized correctly? Also, XMLHttpRequest is part of the browser API, but not part of the core language. So non-browser JS engines like NodeJS don't have it. Is it possible your setup is trying to run the test in Node itself rather than launching a browser to run the test? – SleepyMurph Mar 03 '16 at 12:53
  • yeah, I believe that is the case. We are trying out some simple tests with jsdom. – Winnemucca Mar 03 '16 at 15:19
  • is it possible to use a path like below. eg using ../../../ It doesn't work for me with dots in the path. "../../../path/to/templates/**/*.html": ["ng-html2js"] stripPrefix: '../.././path', Thanks – kam Sep 16 '16 at 17:51
37

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>');
}));
Robin Goupil
  • 154
  • 1
  • 3
  • 14
Jesus is Lord
  • 14,971
  • 11
  • 66
  • 97
  • 26
    Here is my complaint with this method... Now if we are going to have a big piece of html that we are going to inject as a string into the template cache then what are we gonna do when we change the html on the front end? Change the html in the test as well? IMO that is an unsustainable answer and the reason we went with using the template over templateUrl option. Even though i highly dislike having my html as a massive string in the directive - it is the most sustainable solution to not having to update two places of html. Which doesnt take much imaging that the html can over time not match. – Sten Muchow Apr 05 '14 at 08:32
13

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.

bullgare
  • 1,643
  • 1
  • 21
  • 32
  • 1
    Well that's the correct answer to the original question indeed (that's the one that helped me). – Mat Jan 08 '14 at 17:35
  • Tried this, but passThrough() still didn't work for me. It still gave the "Unexpected request" error. – frodo2975 Dec 29 '15 at 20:43
8

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/'
};
  1. 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('');

        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();
        })
    });
    

    });

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

    python -m SimpleHTTPServer 3502

  3. 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.

isherwood
  • 58,414
  • 16
  • 114
  • 157
Tomas Romero
  • 8,418
  • 11
  • 50
  • 72
  • I was having trouble serving up assets from `localhost/base/specs` and adding a proxy server with `python -m SimpleHTTPServer 3502` running fixed it. You sir are a genius! – pbojinov Jul 31 '13 at 18:59
  • I was getting an empty element returned from $compile in my tests. Other places suggested running $scope.$digest(): still empty. Running $scope.$apply() worked though. I think it was because I am using a controller in my directive? Not sure. Thanks for the advice! Helped! – Sam Simmons Sep 09 '13 at 19:55
7

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');
  }));
});
ferics2
  • 5,241
  • 7
  • 30
  • 46
bartek
  • 2,921
  • 5
  • 26
  • 30
  • First decent solution that does not try to force devs to use Karma. Why would angular guys do something so bad and easily avoidable in the middle of something so cool? pfff – Fabio Milheiro Nov 01 '14 at 14:29
  • I see you add a 'test/mock/**/*.js' and I suppose it is to load all the mocked stuff like services and all ? I'm looking up for ways to avoid code duplication of mocked services. Would you show us a bit more on that ? – Stephane Apr 12 '15 at 12:40
  • don't remember exactly, but there were problably settings for example JSONs for $http service. Nothing fancy. – bartek Apr 14 '15 at 11:30
  • Had this problem today - great solution. We use karma but we also use Chutzpah - no reason we should be forced to use karma and only karma to be able to unit test directives. – lwalden Apr 17 '15 at 22:41
  • We're using Django with Angular, and this worked like a charm to test a directive that loads its templateUrl though `static`, e.g. `beforeEach(preloadTemplate(static_url +'seed/partials/beChartDropdown.html'));` Thanks! – Aleck Landgraf Oct 30 '15 at 20:18
6

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');

};
Tomas Romero
  • 8,418
  • 11
  • 50
  • 72
6

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);
      });
    });
    
glepretre
  • 8,154
  • 5
  • 43
  • 57
4

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

Lior
  • 40,466
  • 12
  • 38
  • 40
2

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.

Nikita
  • 6,019
  • 8
  • 45
  • 54
2

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);
        }));

    });
});
Leonard Brünings
  • 12,408
  • 1
  • 46
  • 66
2

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();
    }));
  });
});
Tim Kindberg
  • 3,605
  • 1
  • 25
  • 26
0

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!

kitolog
  • 44
  • 1
  • 2
  • 5