Testing Directives with Dependencies in Angular

Elliot Winkler

Testing Angular applications is multifaceted. On one hand, it’s fantastic that the Angular team places such a high emphasis on testing and provides tools to stub out various components of your application so that they can be tested in isolation. On the other hand, writing these tests is sometimes difficult, as the tools aren’t well documented, and different components are stubbed out or tested using different APIs that seem to elude memorization. In this post we’ll talk about one case that can be particularly frustrating to test: directives with dependencies.

Let’s say we have a group-activity-checkbox directive. This directive is unique in that it must be present within an activity-feed directive. The body of the directive is kept simple for illustration purposes:

groupActivityCheckboxDirective = ->
  restrict: 'E'
  require: '^activityFeed'

  link: (scope, element, attrs, activityFeedCtrl) ->
    activityFeedCtrl.updateActivities()

angular.module('socialnetwork')
  .directive('groupActivityCheckbox', [
    groupActivityCheckboxDirective
  ])

Now we want to write some tests for this directive. Generally directive tests follow this pattern:

  1. Stub any dependencies of the directive
  2. Compile the directive as an element
  3. Set something on the element, trigger an event, set something on the scope, etc., if necessary
  4. Make an assertion on the element’s attributes or content, or on a dependency being accessed in some way

In order to stub the dependencies on our group-activity-checkbox directive, we first need to identify those dependencies. You may think that we only have one – the activity-feed directive – but in fact we have two, because the require option will cause Angular to look for an ActivityFeedCtrl as well.

Given this, here’s what the pattern would look like for our directive:

  1. Stub activity-feed and ActivityFeedCtrl
  2. Compile group-activity-checkbox in the context of activity-feed
  3. Assert that updateActivities() on the ActivityFeedCtrl instance is called

Now we can start writing the tests:

describe '<group-activity-checkbox>', ->
  [$compile, $scope] = []

  beforeEach ->
    module 'socialnetwork', ->
      # Stub ActivityFeedCtrl
      # Stub <activity-feed>
      return

    inject (_$compile_, $rootScope) ->
      $compile = _$compile_
      $scope = $rootScope.$new()

  compileDirective = ->
    # Use the $compile service to compile <group-activity-checkbox> in
    #   the context of <activity-feed>, then return the element

  it 'calls ActivityFeedCtrl#updateActivities', ->
    # Use compileDirective to compile <group-activity-checkbox>
    # Assert that updateActivities on our fake ActivityFeedCtrl is
    #  called

Notice that we’ve left out some pieces above. Let’s go about filling them in now.

Stubbing ActivityFeedCtrl

Angular gives us a way to register controllers using $controllerProvider.register. Re-registering a controller results in overriding it. Given this, here’s how we’d stub ActivityFeedCtrl:

[ActivityFeedCtrl] = []

beforeEach ->
  class ActivityFeedCtrl
    updateActivities: ->

  module 'socialnetwork', ($controllerProvider) ->
    $controllerProvider.register 'ActivityFeedCtrl', ActivityFeedCtrl

Stubbing <activity-feed>

Unfortunately, stubbing directives that are dependencies of other directives isn’t as straightforward as it seems.

Angular also gives us a way to register directives using $compileProvider.directive. This is the method called when you register a directive with the angular.module(...).directive(...) syntax, and you can technically use it directly in tests, but doing so will lead to frustration. One might think that, since $controllerProvider.register and other tools override a given component, $compileProvider.directive works the same way. But this isn’t true.

If we take a closer look at this method, here’s what we learn:

  • A directive may have more than one instance. Calling $compileProvider.directive twice with the same name but different factories results in two instances of that directive being registered.
  • The first time you register a directive, Angular will register a corresponding service, named after the directive and suffixed with the word “Directive”. This service (a factory function) returns all of the instances of the directive in the form of configuration objects (which are then used later by Angular to compile the directive).
  • Angular expects these configuration objects to be normalized. For instance, if the priority of the directive has not been specified, it needs to default to 0, and if a controller has been specified and require has not been specified, require must be set to the name of the directive itself.

Given this information, let’s make a helper function that will allow us to really override a directive. Note that we’ve simplified the logic in $compileProvider.directive – completing it is left as an exercise for the reader – but it suits our needs just fine right now:

overrideDirective = ($provide, name, options = {}) ->
  serviceName = name + 'Directive'
  $provide.factory serviceName, ->
    directive = angular.copy(options)
    directive.priority ?= 0
    directive.name = name
    if !directive.require? && directive.controller?
      directive.require = directive.name
    [directive]

Now here’s how we’d use this function:

module 'socialnetwork', ($provide) ->
  overrideDirective $provide, 'activityFeed',
    restrict: 'E'
    controller: 'ActivityFeedCtrl'
    template: '<group-activity-checkbox>'
  return

Filling in the remaining pieces

To complete our tests, we need to ensure that compileDirective returns the element that represents the group-activity-checkbox directive. Because group-activity-checkbox has to be compiled in the context of activity-feed, we first compile activity-feed and then pull group-activity-checkbox out of it. Then, we write the test itself, making use of compileDirective.

Here is what our finished tests looks like:

overrideDirective = ($provide, name, options = {}) ->
  serviceName = name + 'Directive'
  $provide.factory serviceName, ->
    directive = angular.copy(options)
    directive.priority ?= 0
    directive.name = name
    if !directive.require? && directive.controller?
      directive.require = directive.name
    [directive]

describe '<group-activity-checkbox>', ->
  [$compile, $scope, ActivityFeedCtrl] = []

  beforeEach ->
    class ActivityFeedCtrl
      updateActivities: ->

    module 'socialnetwork', ($controllerProvider, $provide) ->
      $controllerProvider.register 'ActivityFeedCtrl', ActivityFeedCtrl
      overrideDirective $provide, 'activityFeed',
        restrict: 'E'
        controller: 'ActivityFeedCtrl'
        template: '<group-activity-checkbox>'
      return

    inject (_$compile_, $rootScope) ->
      $compile = _$compile_
      $scope = $rootScope.$new()

  compileDirective = ->
    parentElement = $compile('<activity-feed>')($scope)
    parentElement.children().eq(0)

  it 'calls ActivityFeedCtrl#updateActivities', ->
    spyOn(ActivityFeedCtrl.prototype, 'updateActivities')
    compileDirective()
    expect(ActivityFeedCtrl.prototype.updateActivities).toHaveBeenCalled()

(For an interactive version, see the plunker.)