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?
A mock directive in Angular tests can be created by MockDirective function. The mock directive has the same interface as its original directive, but all its methods are dummies. In order to create a mock directive, pass the original directive into MockDirective function.
As you can see, the directive defines a api property which is attached to a JavaScript object with functions on it. From the outside of the directive you can now bind to that API object and invoke operate using its functions.
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
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!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With