Tutorials

AngularJS Testing Tips and Tricks | ng-html2js

Key Takeaways

  • Use karma-ng-html2js-preprocessor to load real template files into AngularJS’s $templateCache before testing rendering or directive behavior.
  • The generated cache key must match templateUrl exactly. stripPrefix changes the cache key, not the directive’s URL.
  • Load both the application module and the generated templates module before injecting services and compiling the directive.
  • Use $httpBackend when the request itself matters; preload templates when it doesn’t.

To test an AngularJS directive that uses templateUrl in Karma, preprocess its HTML into a named AngularJS module, then load that module in your spec setup. karma-ng-html2js-preprocessor generates JavaScript that registers the template content in $templateCache, avoiding a backend request for a matching URL.

The familiar Unexpected Request: GET failure usually means AngularJS’s mock HTTP backend received a template request your test neither cached nor configured a response for. It isn’t evidence that Karma cannot serve asynchronous templates.

Installing the Plugin in a Legacy Project

Start with your project’s known-passing dependency set. The examples assume you already have angular, angular-mocks, karma, jasmine-core, karma-jasmine, and karma-chrome-launcher, plus an available Chrome installation. Keep angular-mocks aligned with your AngularJS release.

The plugin’s package metadata identifies version 1.0.0 and declares a Karma peer dependency of >=0.9, but no Node.js engines range. That broad peer range is not proof of compatibility with your current Node.js, Karma, and browser combination.

If you’re evaluating the plugin for an existing suite, install the documented release explicitly:

npm install --save-dev --save-exact karma-ng-html2js-preprocessor@1.0.0

As documented by npm install, --save-dev records a development dependency and --save-exact saves an exact version. Review the lockfile change, then validate the suite in CI before adopting it.

Record the exact Node.js, AngularJS, angular-mocks, Karma, and plugin versions from that passing run. Validate these examples against your project’s dependency set before relying on them. If you need the surrounding runner setup, see testing AngularJS with Jasmine and Karma.

Create a Directive With an External Template

Use this small example to establish the complete path from source file to rendered output. Save the directive as app/example.directive.js:

'use strict';

angular.module('demoApp', [])
  .directive('myDirective', function () {
    return {
      restrict: 'E',
      templateUrl: 'views/example.html'
    };
  });

Save its template as app/views/example.html:

<p class="message">{{ message }}</p>

The file lives under app/, but the directive requests views/example.html. Your test configuration must account for that difference without changing the application’s production URL.

Configuring Karma to Include Generated Templates

Place this complete configuration at the project root as karma.conf.cjs. It uses Karma’s CommonJS configuration format and an explicit plugin list; adapt the application file paths to your project.

module.exports = function (config) {
  config.set({
    basePath: '',
    frameworks: ['jasmine'],

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

    files: [
      { pattern: 'node_modules/angular/angular.js', watched: false },
      { pattern: 'node_modules/angular-mocks/angular-mocks.js', watched: false },
      'app/example.directive.js',
      {
        pattern: 'app/views/**/*.html',
        type: 'js',
        included: true,
        served: true,
        watched: true
      },
      'test/**/*.spec.js'
    ],

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

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

    browsers: ['ChromeHeadless'],
    reporters: ['progress'],
    autoWatch: false,
    singleRun: true
  });
};

With those dependencies installed, run the local Karma executable from the project root:

./node_modules/.bin/karma start karma.conf.cjs

The explicit filename selects this configuration. singleRun: true makes Karma run the tests and exit; the Karma configuration reference documents that behavior and the configuration options.

Why the File Ordering and Pattern Options Matter

Karma’s file configuration controls inclusion, serving, and ordering. Here, AngularJS loads first, followed by angular-mocks, the application code, generated template JavaScript, and the specs. In a larger application, list module-definition files before files that extend those modules; alphabetical glob ordering is not dependency resolution.

  • preprocessors selects the HTML files to transform, but those files must also appear in files.
  • type: 'js' explicitly includes the transformed content as JavaScript.
  • included: true executes that JavaScript in the test page. Merely serving a file does not register its AngularJS module.
  • served: true lets Karma serve the transformed file to the browser.

The explicit plugins list includes the preprocessor package. If your existing configuration already has that list, add the plugin without removing adapters, launchers, or reporters the rest of your suite needs.

Match the $templateCache Key to templateUrl

The most important configuration detail is the key under which the HTML is cached. The preprocessor implementation derives that key from the source path relative to Karma’s basePath, then applies the configured path transformation.

app/views/example.html
        |
        | stripPrefix: 'app/'
        v
views/example.html
        |
        | generated 'templates' module runs
        v
$templateCache.put('views/example.html', HTML)
        |
        | exact match
        v
templateUrl: 'views/example.html'

stripPrefix: 'app/' removes the leading app/ from the generated key. It does not rewrite templateUrl, move the file, or configure a server route.

For this example, /views/example.html, app/views/example.html, and views/example.html?v=1 are different keys. Don’t add Karma’s /base/ serving prefix unless that prefix actually appears in the URL your application requests.

This process wraps HTML as a JavaScript string and registers cache entries. It does not compile AngularJS bindings ahead of time; the test still uses $compile to compile and link the directive.

In Your Spec Files: Load Both Modules and Assert Rendering

Save this as test/example.spec.js. It follows the module-loading, injection, compilation, and digest pattern in AngularJS’s official unit-testing guide.

describe('myDirective with an external template', function () {
  'use strict';

  var $compile;
  var $rootScope;
  var $templateCache;
  var scope;
  var element;

  beforeEach(angular.mock.module('demoApp', 'templates'));

  beforeEach(angular.mock.inject(function (
    _$compile_, _$rootScope_, _$templateCache_
  ) {
    $compile = _$compile_;
    $rootScope = _$rootScope_;
    $templateCache = _$templateCache_;
    scope = $rootScope.$new();
  }));

  afterEach(function () {
    if (scope) {
      scope.$destroy();
    }
    if (element) {
      element.remove();
    }
  });

  it('renders the external template and its binding', function () {
    expect($templateCache.get('views/example.html')).toBeDefined();

    scope.message = 'Hello from the template';
    element = $compile('<my-directive></my-directive>')(scope);
    $rootScope.$digest();

    expect(element.find('p').text()).toBe('Hello from the template');
  });
});

demoApp contains the directive; templates contains the generated cache registrations. Load both before angular.mock.inject creates the test injector. You don’t need to add the test-only templates module as a production application dependency.

The underscore-wrapped parameters resolve to the corresponding services while allowing assignment to declared outer variables. The digest processes the template promise and binding updates before the assertion; caching the template doesn’t eliminate that step.

The short module alias form, beforeEach(module('templates')), is also available in the Jasmine browser setup. Using angular.mock.module makes the origin of the helper explicit.

Common Mistakes: An Unexpected GET Still Appears

Start with the exact URL in the error, not the location of the HTML file on disk.

  1. Check the cache before compilation. The spec’s $templateCache.get('views/example.html') assertion should pass. If it fails, inspect the template glob, preprocessing configuration, generated module name, and module loading.
  2. Compare the requested URL character for character. Check leading slashes, capitalization, prefixes, and query strings. If templateUrl is a function, inspect the URL it returns for the attributes used in this test.
  3. Check which template failed. A cached outer template can contain another directive or ng-include that requests a different template. Include that template in the preprocessing glob too.
  4. Check execution, not just serving. A template pattern with included: false will not automatically execute the generated module registration.

For a quick diagnostic, add this inside the existing describe block and replace the string with the URL from the failure:

// Fragment: uses $templateCache injected by the surrounding suite.
it('caches the exact URL reported by the failing request', function () {
  var requestedUrl = 'views/example.html';
  expect($templateCache.get(requestedUrl)).toBeDefined();
});

If this passes but compilation still reports an unexpected GET, compare the URL in that new failure with the key you checked. Don’t mask a path mismatch with a catch-all HTTP response.

Preloaded Templates Versus $httpBackend

Choose the approach based on what the test is intended to prove. Preloading and HTTP mocking solve different testing problems.

ApproachBest suited toMaintenance trade-off
Preprocess real HTML into $templateCacheRendering, bindings, and directive behavior using the actual templateKeep cache-key mapping correct; template changes are reflected in the test input.
Put a small fixture directly into $templateCacheFocused tests intentionally independent of production markupThe fixture can drift from the real template.
Respond through $httpBackendTests asserting a template request or behavior around its responseConfigure the expected URL and response, then flush and verify requests.

AngularJS’s ngMock HTTP backend provides expectGET(url).respond(status, body) to assert a request and whenGET(url).respond(status, body) to supply an allowed response. Use flush() to deliver pending responses, and verify outstanding expectations and requests in teardown. Our $httpBackend tutorial with an example covers that workflow.

Don’t preload the same URL in a test that expects a backend GET: the cache hit prevents that request. Conversely, a rendering test using a cached template needs no $httpBackend.flush() for that template; flushing with no pending requests throws.

Neither approach proves that your deployed server exposes the correct template URL. Keep a separate integration or browser test for deployment routing and asset delivery.

FAQ

How do I test an AngularJS directive that uses templateUrl?

Preprocess the external HTML into an AngularJS module with karma-ng-html2js-preprocessor. Load that module and the directive’s application module, compile the element, run a digest, and assert the rendered output.

How does karma-ng-html2js-preprocessor populate $templateCache?

It converts HTML files into JavaScript that registers AngularJS module run blocks. When your test injector loads the generated module, those blocks put the template strings into $templateCache under their generated keys.

Why does my AngularJS Karma test still show Unexpected Request: GET?

The requested template URL may not match a populated cache key, or the generated module may not have been included and loaded. Check the exact URL from the error with $templateCache.get before compiling, and check any nested templates separately.

How should stripPrefix match a directive’s templateUrl?

Configure stripPrefix so removing the source-path prefix produces the directive’s exact templateUrl. For example, stripping app/ from app/views/example.html produces views/example.html; it does not change the directive itself.

Should I use $httpBackend or preloaded templates for AngularJS directive tests?

Preload real templates when testing rendering or directive behavior. Use $httpBackend when you need to assert a request or control its response, and leave that template URL out of the cache for the test.

Further Reading

Written by Sam Erickson

Software engineer. I write every article on StackChief myself. More about me