Angular Directive Testing Best Practices

Getting in the habit of testing is hard, but I’ve found that a big turning point for me when I start testing in new languages and tools is to identify patterns in how tests are written. Once I know a pattern for testing something, it’s easier to walk through each step to write my tests. Just like anything else, it’s more approachable when you break it apart into smaller pieces.

When testing directives, this is the main pattern I see:

  1. Mock
  2. Compile
  3. Interact
  4. Assert

An Example Directive

I’m going to walk through each step in the directive testing pattern for a directive I wrote recently. Here is the source of the radioButton directive, which provides a way to use custom style and markup for a radio form control.

angular
  .module('kit.forms', [])
  .directive('radioButton', radioButtonDirective);

function radioButtonDirective() {
  return {
    require: '?ngModel',
    link: function(scope, element, attributes, ngModelCtrl) {
      element.addClass('button');

      ngModelCtrl.$render = function() {
        element.toggleClass('primary', angular.equals(ngModelCtrl.$modelValue, scope.$eval(attributes.radioButton)));
      };

      element.bind('click', function() {
        if (!element.hasClass('active')) {
          scope.$apply(function () {
            ngModelCtrl.$setViewValue(scope.$eval(attributes.radioButton));
            ngModelCtrl.$render();
          });
        }
      });
    }
  };
}

Note: This example shows some of the power of ngModelController, which I think is pretty cool. If you’re interested in learning more about what you can do with ngModel, send me an email or let me know in the comments.

Mock

The first thing we need to do is mock out our dependencies. In this example, we are mocking out the kit.forms module and injecting a new instance of $rootScope to use later in our tests.

describe('Forms module', function() {
  var element, scope;

  beforeEach(module('kit.forms'));

  beforeEach(inject(function($rootScope) {
    scope = $rootScope.$new();
  }));

  // ...
});

Compile

Directives need to be compiled in order to make assertions on the HTML they produce. This can be done by injecting the $compile service and running a string of HTML through it. After it is compiled, the digest cycle needs to be kicked off with a call to $digest() from the $scope instance created earlier.

describe('[required]', function() {
  beforeEach(inject(function($compile) {
    element = angular.element(
      '<div class="button-group">' +
        '<label ng-model="model" radio-button="'yes'">Yes</label>' +
        '<label ng-model="model" radio-button="'no'">No</label>' +
        '<label ng-model="model" radio-button="'maybe'">Maybe</label>' +
      '</div>'
    );

    $compile(element)(scope);
    scope.$digest();
  }));

  // ...
});

Interact

Now you can script some interactions on the compiled HTML before you make assertions. Storing a reference to the element(s) you want to manipulate and make assertions against at the beginning of your specs is usually helpful.

it('applies the "primary" class when clicked', function() {
  var radios = element.find('.button');

  radios.eq(1).click();
  // ...

  radios.eq(2).click();
  // ...
});

Assert

Finally, we can make assertions that the markup has changed the way it’s expected to.

it('applies the "primary" class when clicked', function() {
  var radios = element.find('.button');

  radios.eq(1).click();
  expect(radios.eq(1)).toHaveClass('primary');
  expect(radios.eq(0)).not.toHaveClass('primary');
  expect(radios.eq(2)).not.toHaveClass('primary');

  radios.eq(2).click();
  expect(radios.eq(2)).toHaveClass('primary');
  expect(radios.eq(1)).not.toHaveClass('primary');
});

Sometimes the interaction step isn’t necessary for making assertions. For example, it’s usually best to assert that a directive initially compiles the way you expect it to.

it('adds the "button" class', function() {
  expect(element.find('.button').length).toBe(3);
});

E2E Testing Directives

When I first started testing directives I thought I was doing it wrong because I was testing the DOM but I wasn’t using a true integration testing tool, like Protractor. However, I realized a lot of times it’s hard to know the context a directive is going to be used in so it’s difficult to end-to-end test effectively. In most cases it’s easier to avoid an integration testing tool for an individual directive test and just rely on that for flexing the main flows of your SPAs.

Other Patterns

Directives are easily the most difficult thing to test in Angular, but I’ll be sharing some other Angular testing patterns and best practices I use over the next few weeks. Make sure to subscribe to email updates if you want to keep up with this series of articles on Angular testing and please share any other patterns you’ve seen in the comments.

  • chrisjaure

    Nice! I like how you distilled it down to a simple 4-step process. Here are the some handy sublime text snippets I use for testing different types of angular components: https://gist.github.com/chrisjaure/6515906

    A couple corrections I would make:

    1. Calling `beforeEach(module(‘kit.forms’));` doesn’t mock the module; it will load it along with any other directives, controllers, etc. attached to it. You’ll still need to mock any specific dependencies if required.

    2. The string concatenation looks messed up in the markup in the compile step.

    • Thanks for taking the time to share your thoughts and those snippets, @chrisjaure:disqus! One of the main pain points for me with my Angular tests is how repetitive they with all the setup you need to do just to get to the point where you can start making assertions. These will definitely help alleviate the pain :)

      Good point on #1. I’ll make sure to update the article to clarify exactly what’s happening on there. On #2, I actually had the single quotes escaped for the `radio-button` values when I published this, but I think they were lost in translation. Thanks for catching that and I’ll see if I can fix them.