How do you mock directives to enable unit testing of higher level directive?

Unit TestingAngularjsAngularjs Directive

Unit Testing Problem Overview


In our app we have several layers of nested directives. I'm trying to write some unit tests for the top level directives. I've mocked in stuff that the directive itself needs, but now I'm running into errors from the lower level directives. In my unit tests for the top level directive, I don't want to have to worry about what is going on in the lower level directives. I just want to mock the lower level directive and basically have it do nothing so I can be testing the top level directive in isolation.

I tried overwriting the directive definition by doing something like this:

angular.module("myModule").directive("myLowerLevelDirective", function() {
	return {
		link: function(scope, element, attrs) {
			//do nothing
		}
	}
});

However, this does not overwrite it, it just runs this in addition to the real directive. How can I stop these lower level directives from doing anything in my unit test for the top level directive?

Unit Testing Solutions


Solution 1 - Unit Testing

Directives are just factories, so the best way to do this is to mock the factory of the directive in using the module function, typically in the beforeEach block. Assuming you have a directive named do-something used by a directive called do-something-else you'd mock it as such:

beforeEach(module('yourapp/test', function($provide){
  $provide.factory('doSomethingDirective', function(){ return {}; });
}));

// Or using the shorthand sytax
beforeEach(module('yourapp/test', { doSomethingDirective: {} ));

Then the directive will be overridden when the template is compiled in your test

inject(function($compile, $rootScope){
  $compile('<do-something-else></do-something-else>', $rootScope.$new());
});

Note that you need to add the 'Directive' suffix to the name because the compiler does this internally: https://github.com/angular/angular.js/blob/821ed310a75719765448e8b15e3a56f0389107a5/src/ng/compile.js#L530

Solution 2 - Unit Testing

The clean way of mocking a directive is with $compileProvider

beforeEach(module('plunker', function($compileProvider){
  $compileProvider.directive('d1', function(){ 
    var def = {
      priority: 100,
      terminal: true,
      restrict:'EAC',
      template:'<div class="mock">this is a mock</div>',
    };
    return def;
  });
}));

You have to make sure the mock gets a higher priority then the directive you are mocking and that the mock is terminal so that the original directive will not be compiled.

priority: 100,
terminal: true,

The result would look like the following:

Given this directive:

var app = angular.module('plunker', []);
app.directive('d1', function(){
  var def =  {
    restrict: 'E',
    template:'<div class="d1"> d1 </div>'
  }
  return def;
});

You can mock it like this:

describe('testing with a mock', function() {
var $scope = null;
var el = null;

beforeEach(module('plunker', function($compileProvider){
  $compileProvider.directive('d1', function(){ 
    var def = {
      priority: 9999,
      terminal: true,
      restrict:'EAC',
      template:'<div class="mock">this is a mock</div>',
    };
    return def;
  });
}));

beforeEach(inject(function($rootScope, $compile) {
  $scope = $rootScope.$new();
  el = $compile('<div><d1></div>')($scope);
}));

it('should contain mocked element', function() {
  expect(el.find('.mock').length).toBe(1);
});
});

A few more things:

  • When you create your mock, you have to consider whether or not you need replace:true and/or a template. For instance if you mock ng-src to prevent calls to the backend, then you don't want replace:true and you don't want to specify a template. But if you mock something visual, you might want to.

  • If you set priority above 100, your mocks's attributes will not be interpolated. See $compile source code. For instance if you mock ng-src and set priority:101, then you'll end-up with ng-src="{{variable}}" not ng-src="interpolated-value" on your mock.

Here is a plunker with everything. Thanks to @trodrigues for pointing me in the right direction.

Here is some doc that explains more, check the "Configuration Blocks" section. Thanks to @ebelanger!

Solution 3 - Unit Testing

Due to the implementation of the directive registration, it does not seem possible to replace an existing directive by a mocked one.

However, you have several ways to unit test your higher level directive without interference from lower level directives :

  1. Do not use lower level directive in your unit test template :

If your lower level directive is not added by your higher level directive, in your unit test use a template with only you higer-level-directive :

var html = "<div my-higher-level-directive></div>";
$compile(html)(scope);

So, lower level directive will not interfere.

  1. Use a service in your directive implementation :

You can provide the lower level directive linking function by a service :

angular.module("myModule").directive("myLowerLevelDirective", function(myService) {
    return {
        link: myService.lowerLevelDirectiveLinkingFunction
    }
});

Then, you can mock this service in your unit test to avoid interference with your higher level directive. This service can even provide the whole directive object if needed.

  1. You can overwrite your lower level directive with a terminal directive :

    angular.module("myModule").directive("myLowerLevelDirective", function(myService) { return { priority: 100000, terminal: true, link: function() { // do nothing } } });

With the terminal option and a higher priority, your real lower level directive will not be executed. More infos in the directive doc.

See how it works in this Plunker.

Solution 4 - Unit Testing

You can modify your templates inside $templateCache to remove any lower level directives:

beforeEach(angular.mock.inject(function ($templateCache) {
  $templateCache.put('path/to/template.html', '<div></div>');
}));

Solution 5 - Unit Testing

Being forced to think about this more myself, I have come up with a solution that fills our needs. All of our directives are attributes, so I created an attributeRemover directive for use during the unit tests. It looks something like this:

angular.module("myModule").directive("attributeRemover", function() {
	return {
		priority: -1, //make sure this runs last
		compile: function(element, attrs) {
			var attributesToRemove = attrs.attributeRemover.split(",");
			angular.forEach(attributesToRemove, function(currAttributeToRemove) {
				element.find("div[" + currAttributeToRemove + "]").removeAttr(currAttributeToRemove);
			});
		}
	}
});

Then the html for the directive I'm testing looks something like this:

<div my-higher-level-directive attribute-remover="my-lower-level-directive,another-loweler-level-directive"></div>

So, when my-higher-level-directive gets compiled the attribute-remover will have already removed the attributes for the lower level directives and thus I don't have to worry about what they are doing.

There's probably a more robust way of doing this for all kinds of directives (not just attribute ones) and I'm not sure if this works if only using the built-in JQLite, but it works for what we need.

Solution 6 - Unit Testing

Loved Sylvain's answer so much I had to turn it into a helper function. Most often, what I need is to kill off a child directive so that I can compile and test the parent container directive without its dependencies. So, this helper lets us do that:

function killDirective(directiveName) {
  angular.mock.module(function($compileProvider) {
    $compileProvider.directive(directiveName, function() {
      return {
        priority: 9999999,
        terminal: true
      }
    });
  });
}

With that, you can completely disable a directive by running this before the injector gets created:

killDirective('myLowerLevelDirective');

Solution 7 - Unit Testing

Here is another small idea. Just put this code in jasmine helpers (coffee script)

window.mockDirective = (name, factoryFunction) ->
  mockModule = angular.module('mocks.directives', ['ng'])
  mockModule.directive(name, factoryFunction)

  module ($provide) ->
    factoryObject = angular.injector([mockModule.name]).get("#{name}Directive")
    $provide.factory "#{name}Directive", -> factoryObject
    null

And use it:

beforeEach mockDirective, "myLowerLevelDirective", ->
  link: (scope, element) ->

This will completely remove all other implementations of given directive, giving a full access to test passed arguments to the directive. FOr example, mm.foundation alert directive can be mocked with:

beforeEach mockDirective 'alert', ->
  scope:
    type: '='

and then tested:

expect(element.find('alert').data('$isolateScopeNoTemplate').type).toEqual 

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
Questiondnc253View Question on Stackoverflow
Solution 1 - Unit TestingtrodriguesView Answer on Stackoverflow
Solution 2 - Unit TestingSylvainView Answer on Stackoverflow
Solution 3 - Unit TestingBastien CaudanView Answer on Stackoverflow
Solution 4 - Unit TestingCinamonasView Answer on Stackoverflow
Solution 5 - Unit Testingdnc253View Answer on Stackoverflow
Solution 6 - Unit TestingMerottView Answer on Stackoverflow
Solution 7 - Unit TestingBroiSatseView Answer on Stackoverflow