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

Solution 1:

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:

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!