6/23/2015

Tweak the Angular Test By Controlling the Template

We recently finished bringing our test coverage up to 90%+ on our Angular project. The team worked hard and I am proud of our accomplishments. Going forward we are using TDD and this has proven to be an interesting experience that has provided new challenges. We have found building Angular directives through test driven development to be a little challenging since the templates haven't been built yet. We aren't going to know what to build right away, and we are loathe to break our rhythm and constantly be updating a template file. A bigger problem is the template creating an external dependency which is never a good thing for unit tests. When it comes time to do e2e testing and using Selenium or Protractor then the whole template needs to be run. If I'm starting with unit tests to define my code, how can I use a template without these problems? We found one method that we like - the $templateCache.

An unexpected benefit has been the ability to easily build our templates as we go. We don't need to build the template ahead of time and fracture our TDD process. Our templates can iterate with our code, and we can focus on parts of the template that match the code being worked on. We can rely on end to end tests for total integration. In the unit test we can have total control over the template which limits variability that could be caused by a full external template.

We use ngTestHarness for all of our tests. I'm going to stick with that for our example. ngTestHarness has greatly simplified the unit testing process with Angular and obfuscates a lot of the extra processes that Angular requires. If you haven't taken a look I suggest you do, of course I am very biased.

Here is our code for the sample:

angular.module('sample', ['ngSanitize'])
.directive('sampleDemo', function () {
    return {
        restrict: 'EA',
        replace: true,
        scope: {
            message: '@',
        },
        templateUrl: 'template.html'
    }
});

This is a very simple directive whose only purpose is to put a string of text on a page. Now to add the test:

//Here is the directive to be transformed
var element='<sample-demo message="Hello"></sample-demo\>'

describe("Load Sample\n", function () {
   var harness = new ngTestHarness([
    'sample'
   ]);
  
        //first test
   it('Expect innerHTML to contain message', function () {
      harness.getProvider('$templateCache').put('template.html', '<div>{{message}}</div>');
      expect(harness.compileElement(element).html()).toContain('Hello');
   });
  });

The $templateCache is an angular service that is designed to improve performance. The first time a template is loaded onto the screen, it is also loaded into the $templateCache. The next time the template is needed it is pulled directly from the cache. The Angular developers provided a simple API to interface with this service, and this is what we are using for our test. Each test can update the 'template.html' stored in the cache to meet its testing need.

Let's see how we can expand the directive and the test at the same time to keep the two in sync. Let's start by adding our new scope variable to the element we are testing against:

var element='<sample-demo message="Hello" message2="Reader"></sample-demo>'

Now, let's add message2 to the scope:

        scope: {
            message: '@',
            message2: '@'
        },

Finally, we add a new test, and have it modify our working template:

   it('Expect template change to modify html', function (){
      harness.getProvider('$templateCache').put('template.html', '<div>{{message}} {{message2}}</div>');
      expect (harness.compileElement(element).html()).toContain('Hello Reader'); 
   });

Our template has been expanded to match the new scope variable we added. I could also be updating element as I go, but that didn't seem as necessary. My TDD can continue and I can save worrying about the UI for later when I am ready for it. Even better, I will have a tested and working template as soon as I need it.

The code can be seen in action at Cloud9.