This is me

Hi! I'm Dag-Inge.

In my day job, I work on appear.in, a video chat service built with AngularJS and WebRTC. Recently, our application has become more complex, and our HTML templates ended up having a lot of state in them. We could no longer just use unit testing to verify our application logic, we also needed to test the state in our templates.

For our unit testing, we landed on using Jasmine and Karma, and this has worked fairly well for us. So when we wanted to test our templates, we wanted to use the same tools.

Setting up Karma to serve templates

Luckily, testing anything in Angular is pretty straight forward. However, integrating HTML templates from templateUrls was not that easy. For those familiar with Karma from before, you can use the Karma server to serve static files needed to be used in your testing. This means that you do not need to fire up your own test server and keep that running.

Configuring Karma to serve files is as simple as declaring which files you want it to serve. This works great for *.js files, but what about HTML and other static files? It turns out that Karma only really likes serving JavaScript files, so we have to do some clever workarounds to make Angular templates, which are just regular HTML files, work.

To make Karma serve HTML templates, we have to use a preprocessor that turns HTML templates into JavaScript strings and registers them with Angular’s $templateCache. This means that Angular can access the templates without having to make separate HTTP requests. All we need to do then is serve the processed template JavaScript.

After some research, I found karma-ng-html2js-preprocessor, a npm package that does the above, all from the karma configuration. Run npm install karma-ng-html2js-preprocessor --save-dev in the root path of the project you want to test. Now all you have to do is to set it up in your karma.conf.js file.

 1 module.exports = function (config) {
 2     config.set({
 3         preprocessors: {
 4             'path/to/html/templates/**/*.html': ['ng-html2js']
 5         },
 6 
 7         ngHtml2JsPreprocessor: {
 8             // setting this option will create only a single module that contains templates
 9             // from all the files, so you can load them all with module('foo')
10             moduleName: 'templates'
11         },
12 
13         files: [
14             // ...
15             'path/to/html/templates/**/*.html'
16         ],
17 
18         // ...
19     });
20 };

Writing tests with Jasmine

Now that we have gotten the file serving out of the way, let’s take a look how to write tests for our templates. Say we have the following HTML template we want to test:

1 <h1 ng-bind="header"></h1>
2 <p ng-bind="text"></p>

Now we want to verify that our $scope variables are not hardcoded in, and are rendered as expected into our template. To do that, we would write the following test.

 1 describe("Directive:", function () {
 2 
 3     beforeEach(angular.mock.module("yourAppModule"));
 4 
 5     describe("template", function () {
 6         var $compile;
 7         var $scope;
 8         var $httpBackend;
 9 
10         // Load the templates module
11         beforeEach(module('templates'));
12 
13         // Angular strips the underscores when injecting
14         beforeEach(inject(function(_$compile_, _$rootScope_) {
15             $compile = _$compile_;
16             $scope = _$rootScope_.$new();
17         }));
18 
19         it("should render the header and text as passed in by $scope",
20         inject(function() {
21             // $compile the template, and pass in the $scope.
22             // This will find your directive and run everything
23             var template = $compile("<div your-directive></div>")($scope);
24 
25             // Set some values on your $scope
26             $scope.header = "This is a header";
27             $scope.text = "Lorem Ipsum";
28 
29             // Now run a $digest cycle to update your template with new data
30             $scope.$digest();
31 
32             // Render the template as a string
33             var templateAsHtml = template.html();
34 
35             // Verify that the $scope variables are in the template
36             expect(templateAsHtml).toContain($scope.header);
37             expect(templateAsHtml).toContain($scope.text);
38 
39             // Do it again with new values
40             var previousHeader = $scope.header;
41             var previousText = $scope.text;
42             $scope.header = "A completely different header";
43             $scope.text = "Something completely different";
44 
45             // Run the $digest cycle again
46             $scope.$digest();
47 
48             templateAsHtml = template.html();
49 
50             expect(templateAsHtml).toContain($scope.header);
51             expect(templateAsHtml).toContain($scope.text);
52             expect(templateAsHtml).toNotContain(previousHeader);
53             expect(templateAsHtml).toNotContain(previousText);
54 
55         }));
56 
57     });
58 });

Some caveats

So, this all sounds very easy now doesn’t it? And yes, it is easy, but there are also some caveats you need to know about should you choose to test your templates this way.

The first one is non-html content. If you, like us, use ng-include to insert svg data into the DOM (so you can style it with CSS), you are going to have a bad time. PhantomJS will throw an error and exit, so you have to find another way. What I did was mock the file path in $httpBackend so that it responded with an empty string for all instances of .svg.

$httpBackend.whenGET('/path/to/a.svg').respond("");

I was not able to find a general solution for ignoring all .svg files based on wildcard, but I suspect that it’s possible.

The second one is that the tests can be brittle. When you want to test if an ng-if is inserted or not, you must rely on checking for other things other than $scope variables, such as CSS classes. This means that a well-meaning developer can break the build by changing a CSS class, which is not very friendly. Proceed with caution.

Hopefully the guide was useful for you! And remember, always be test.in™!