Testing Angular Directives with Karma, Mocha, and Phantom

All Code on Github

The entire code from this article is on github.

Introduction

Angular Directives are part of the web part/components group of client-side  tools that allow quick additions to web pages. The idea is that with a very simple and short addition to an html page, complex functionality and UX are available.

Imagine a user login form with the traditional validation contained in a html template and Angular controller. The main, calling web page’s html could just include <login></login> to bring in that rich validation and display. Directives are wonderful for encapsulating the complexity away from the containing HTML.

Testing the directive is trickier. Granted the controller isn’t difficult to test. But the compiled html and scope are not so easy.

Perhaps it is enough to test the controller, and depend on the browser and the Angular library to manage the rest.  You could definitely make that case. Or perhaps you leave the compiled directive testing to the end to end (e2e) tests. That is also a fair. If either of these solutions doesn’t work for you, this article will explain 3 ways to test the compiled Angular directive.

Template
When you build the directive, you can choose static (hopefully short) html in the definition of the directive.  Notice that {{data}} will be replaced with the $scope.data value from the directive.
exports.user = function() {
  return {
    controller: 'userController',
    template: '<div class="user">{{data}}</div>'
    };
};
TemplateUrl
If you have more html or want to separate the html from the Angular directive, templateUrl is the way to go.
exports.menu = function() {
  return {
    controller: 'menuController',
    templateUrl: '/templates/menu.html'
  };
};
The html contained in menu.html is below:
<div class="menu">
    {{data}}
</div>

Compilation

Angular compiles the controller and the template in order to replace the html tag. This compilation is necessary to test the directive.

Prerequisites

This article assumes you have some understanding of javascript, Angular, and unit testing. I’ll focus on how the test was setup and compiled the directive so that it could be tested. You need node and npm to install packages and run scripts. The other dependencies are listed in the package.json files.

Karma, Mocha, Chai, Phantom Test System

Since Angular is a client-side framework, Karma acts as the web server and manages the web browser for html and javascript. Instead of running a real browser, I chose Phantom so everything can run from the command line. Mocha and chai are the test framework and assert library.

While the Angular code is browserified, that doesn’t have any impact on how the directive is tested.

The Source Code and Test

With very little difference, each of the 3 examples is just about the same: a very simple controller that has $scope.data, an html template that uses {{data}} from the scope, and a test that compiles the directive and validates that the {{data}} syntax was replaced with the $scope.data value.

Each project has the same directory layout:

/client angular files to be browserified into /public/app.js
/public index.html, app.js, html templates
/test mocha/chai test file
karma.conf.js karma configuration file
gulpfile.js browserify configuration
package.json list of dependencies and script to build and run test

Testing the static template


The first example tests a template using static html. 

The controller:
exports.userController = function ($scope) {
    $scope.data = "user";
};
The directive: 
exports.user = function() {
  return {
    controller: 'userController',
    template: '<div class="user">{{data}}</div>'
    };
};
The calling html page
<html ng-app="app">
  <head>
    <script type="text/javascript"
      src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0/angular.js">
    </script>
    <script type="text/javascript"
      src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0/angular-route.js">
    </script>
    <script type="text/javascript" src="/app.js"></script>
  </head>
  <body>
      <div>
          <user></user>
      </div> 
  </body>
</html>
The final html page will replace <user></user> with the compiled html from the template with the string “user” replacing {{data}} in the static template html. The mocha test
describe('userDirective', function() {
  var scope;

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

  beforeEach(inject(function ($rootScope) {
      scope = $rootScope.$new();
    }));
    
  it('should exist', inject(function ($compile) {
    element = angular.element('<user></user>');
    compiledElement = $compile(element)(scope);
 
    scope.$digest();

    var dataFromHtml = compiledElement.find('.user').text().trim();
    var dataFromScope = scope.data;
    
    console.log(dataFromHtml + " == " + dataFromScope);

    expect(dataFromHtml).to.equal(dataFromScope);
  }));
});
As part of the test setup, in the beforeEach functions, the test brings in the angular app, brings in the mock library, and sets the scope. Inside the test (the ‘it’ function), the element function defines the directive’s html element, and compiles the scope and the element. The compiled html text value is tested against the scope value – which we expect to be the same. The overall test concept is the same for each of the examples. Get the controller and template, compile it, check it. The details differ in exactly how this is done in the 2 remaining examples.

The 2 TemplateUrl Examples

The first example above relied on the directive definition to load the template html. The next 2 examples use the TemplateUrl property which has the html stored in a separate file so that method won’t work. Both of the next 2 examples use the templateCache to load the template, but each does it in a different way. The first example loads the template and tests the template as though the templateCache isn’t used. This is a good example for apps that don’t generally use the templateCache and developers that don’t want to change the app code in order to test the directive. The templateCache is only used as a convenience for testing. The second example alters the app code by loading the template in the templateCache and browserifying the app with the ‘templates’ dependency code. This is a good way to learn about the templateCache and test it.

Testing TemplateUrl – least intrusive method

The second example stores the html in a separate file instead of in the directive function. As a result, the test (and the karma config file) needs to bring the separate file in before compiling the element with the scope. The controller:
exports.menuController = function ($scope) {
    $scope.data = "menu";
};
The directive:
exports.menu = function() {
  return {
    controller: 'menuController',
    templateUrl: '/templates/menu.html'
  };
};
The template:
<div class="menu">
    {{data}}
</div>
The calling html page only different in that the html for the directive is
<menu></menu>
karma-utils.js
function httpGetSync(filePath) {
  var xhr = new XMLHttpRequest();
  
  var finalPath = filePath;
  
  //console.log("finalPath=" + finalPath);
  
  xhr.open("GET", finalPath, false);
  xhr.send();
  return xhr.responseText;
}

function preloadTemplate(path) {
  return inject(function ($templateCache) {
    var response = httpGetSync(path);
    //console.log(response);
    $templateCache.put(path, response);
  });
}
The file along with the template file are brought in via the karma.conf.js file in the files property:
// list of files / patterns to load in the browser
files: [
    'http://code.jquery.com/jquery-1.11.3.js',
    'https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0/angular.js',
   
    // For ngMockE2E
    'https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0/angular-mocks.js',
    'test/karma-utils.js',
    'test/test.directive.*.js',
    'public/app.js',
    'public/templates/*.html'
],
The mocha test
describe('menuDirective', function() {
  var mockScope;
  var compileService;
  var template;

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

  beforeEach(preloadTemplate('/templates/menu.html'));

  beforeEach(inject(function ($rootScope) {
      scope = $rootScope.$new();
    }));
    
  it('should exist', inject(function ($compile) {
    element = angular.element('<menu></menu>');
    compiledElement = $compile(element)(scope);
 
    scope.$digest();

    var dataFromHtml = compiledElement.find('.menu').text().trim();
    var dataFromScope = scope.data;
    
    console.log(dataFromHtml + " == " + dataFromScope);

    expect(dataFromHtml).to.equal(dataFromScope);
  }));
});
As part of the test setup, in the beforeEach functions, the test brings in the angular app, brings in the mock library, brings in the html template, and sets the scope. The template is brought in via a help function in another file in the test folder called preloadTemplate. Inside the test (the ‘it’ function), the element function defines the directive’s html element, and compiles the scope and the element. The compiled html text value is tested against the scope value – which we expect to be the same. This is the line of the test file that deals with the templateCache:
beforeEach(preloadTemplate('/templates/menu.html')); 
The app and test code do not use the templateCache in any other way. The test itself is almost identical to the first test that uses the static html in the directive.

Testing TemplateUrl – most intrusive method

In this last example, the app.js code in the /client directory is altered to pull in a new ‘templates’ dependency module. The templates module is built in the gulpfile.js by grabbing all the html files in the /public/templates directory and wrapping it in javascript that adds the templates to the templateCache. The test explicitly pulls the template from the templateCache before compilation. gulpfile.js – to build templates dependency into /client/templates.js
var gulp = require('gulp');
var browserify = require('gulp-browserify');

var angularTemplateCache = require('gulp-angular-templatecache');

var concat = require('gulp-concat');
var addStream = require('add-stream');

gulp.task('browserify', function() {
  return gulp.
    src('./client/app.js').
    //pipe(addStream('./client/templates.js')).
    //pipe(concat('app.js')).
    pipe(browserify()).
    pipe(gulp.dest('./public'));
});

gulp.task('templates', function () {
  return gulp.src('./public/templates/*.html')
    .pipe(angularTemplateCache({module:'templates', root: '/templates/'}))
    .pipe(gulp.dest('./client'));
});

gulp.task('build',['templates', 'browserify']);
templates.js – built by gulpfile.js
angular.module("templates").run([
    "$templateCache",  function($templateCache) {
     $templateCache.put("/templates/system.html","<div class=\"menu\">\n    {{data}}\n</div>");
    }
]);
app.js – defines template module and adds it to app list of dependencies
var controllers = require('./controllers');
var directives = require('./directives');
var _ = require('underscore');

// this never changes
angular.module('templates', []);

// templates added as dependency
var components = angular.module('app', ['ng','templates']);

_.each(controllers, function(controller, name)
{ components.controller(name, controller); });

_.each(directives, function(directive, name)
{ components.directive(name, directive); });

require('./templates')
The mocha test
describe('menuDirective', function() {
  var mockScope;
  var compileService;
  var template;

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

  beforeEach(inject(function ($rootScope) {
      scope = $rootScope.$new();
    }));
    
  it('should exist', inject(function ($compile, $templateCache) {
    element = angular.element('<system></system>');
    compiledElement = $compile(element)(scope); 

    // APP - app.js used templates dependency which loads template
    // into templateCache
    // TEST - this test pulls template from templateCache 
    template = $templateCache.get('/templates/system.html'); 
 
    scope.$digest();

    var dataFromHtml = compiledElement.find('.menu').text().trim();
    var dataFromScope = scope.data;
    
    console.log(dataFromHtml + " == " + dataFromScope);

    expect(dataFromHtml).to.equal(dataFromScope);
  }));
});

The Test Results

The test results for each of the 3 projects is checked in to the github project. Karma ran with debug turned on so that the http and file requests could be validated. When you look at the testResult.log files (1 in each of the 3 subdirectories), you want to make sure that the http and file requests that karma made were actually successful. The lines with ‘Fetching’, ‘Requesting’, and ‘(cached)’ are the important lines before the test runs.

36m26 04 2016 11:42:09.318:DEBUG [middleware:source-files]: [39mFetching /home/dina/repos/AngularDirectiveKarma/directiveTemplate/test/test.directive.template.js
[36m26 04 2016 11:42:09.318:DEBUG [middleware:source-files]: [39mRequesting /base/public/app.js?6da99f7db89b4401f7fc5df6e04644e14cbed1f7 /
[36m26 04 2016 11:42:09.318:DEBUG [middleware:source-files]: [39mFetching /home/dina/repos/AngularDirectiveKarma/directiveTemplate/public/app.js
[36m26 04 2016 11:42:09.319:DEBUG [web-server]: [39mserving (cached): /home/dina/repos/AngularDirectiveKarma/directiveTemplate/node_modules/mocha/mocha.js
[36m26 04 2016 11:42:09.329:DEBUG [web-server]: [39mserving (cached): /home/dina/repos/AngularDirectiveKarma/directiveTemplate/node_modules/karma-mocha/lib/adapter.js [36m26 04 2016 11:42:09.331:DEBUG [web-server]: [39mserving (cached): /home/dina/repos/AngularDirectiveKarma/directiveTemplate/test/test.directive.template.js
[36m26 04 2016 11:42:09.333:DEBUG [web-server]: [39mserving (cached): /home/dina/repos/AngularDirectiveKarma/directiveTemplate/public/app.js

During the test, you should see that the $scope.data value (different in each of the 3 tests) is equated to the compiled html as expected, for example, “user = user”:
LOG: 'user == user'
And finally, that the test passed:
Executed 1 of 1 SUCCESS

Popular posts from this blog

Simple WP7 Mango App for Background Tasks, Toast, and Tiles: Code Explanation

Yet once more into the breech (of altered programming logic)

Error : /ScriptResource.axd : Invalid viewstate.