你如何模拟指令来启用更高级指令的unit testing?

在我们的应用程序中,我们有几层嵌套的指令。 我正在尝试为顶级指令编写一些unit testing。 我嘲笑指令本身需要的东西,但现在我遇到了来自较低级指令的错误。 在我的顶级指令的unit testing中,我不想担心底层指令中发生了什么。 我只是想嘲笑较低级别的指令,基本上没有做任何事情,所以我可以单独testing顶级指令。

我尝试通过做这样的事情来覆盖指令定义:

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

但是,这不会覆盖它,它只是运行这个除了真正的指令。 我怎样才能停止这些较低级别的指令做我的unit testing顶级指令的任何东西?

由于指令注册的实施,似乎不可能用嘲弄的指令代替现有的指令。

但是,您可以通过多种方式来unit testing您的高层指令,而不受下层指令的干扰:

1)不要在unit testing模板中使用低级指令:

如果您的低级指令不是由您的高级指令添加的,那么在您的unit testing中只使用您的高级指令来使用模板:

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

所以,下级指令不会干涉。

2)在你的指令实现中使用一个服务

您可以通过服务提供下级指令链接function:

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

然后,您可以在unit testing中嘲笑这项服务,以避免干扰您的上级指令。 如果需要,该服务甚至可以提供整个指令对象。

3)你可以用一个terminal指令覆盖你的低级指令

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

有了terminal选项和更高的优先级,您的真正的低级指令将不会被执行。 更多信息在指令文档 。

看看它是如何工作在这个Plunker 。

指令只是工厂,所以最好的办法是模拟使用module函数的指令工厂,通常在beforeEach模块中。 假设你有一个名为do-do的指令,这个指令叫do-something-else,你可以嘲笑它:

 beforeEach(module('yourapp/test', function($provide){ $provide.factory('doSomethingDirective', function(){ return {}; }); })); // Or using the shorthand sytax beforeEach(module('yourapp/test', { doSomethingDirective: {} )); 

那么当模板在你的testing中被编译时,这个指令将被覆盖

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

请注意,您需要在名称中添加“指令”后缀,因为编译器在内部执行此操作: https : //github.com/angular/angular.js/blob/821ed310a75719765448e8b15e3a56f0389107a5/src/ng/compile.js#L530

嘲笑一个指令干净的方式是$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; }); })); 

你必须确保模拟得到更高的优先级,那么你正在嘲笑的指令,模拟terminal,以便原始指令不会被编译。

 priority: 100, terminal: true, 

结果如下所示:

鉴于这个指令:

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

你可以像这样嘲笑它:

 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); }); }); 

还有几件事情:

  • 当你创build你的模拟,你必须考虑是否需要replace:true和/或template 。 例如,如果你模拟ng-src来阻止对后端的调用,那么你不想replace:true ,你不想指定一个template 。 但是,如果你嘲笑视觉,你可能会想。

  • 如果你将优先级设置为100以上,你的模拟的属性将不会被插值。 请参阅$ compile源代码 。 例如,如果你模拟ng-src并设置priority:101 ,那么你的模拟结果是ng-src="{{variable}}"而不是ng-src="interpolated-value"

这里是一切的一切。 感谢@trodrigues指引我在正确的方向。

这里有一些文档解释更多,检查“configuration块”部分。 感谢@ebelanger!

您可以修改$templateCache以删除任何较低级别的指令:

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

被迫自己多想一想,我想出了一个满足我们需求的解决scheme。 我们所有的指令都是属性,所以我在unit testing中创build了一个attributeRemover指令。 它看起来像这样:

 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); }); } } }); 

然后,我正在testing的指令的html看起来像这样:

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

所以,当my-higher-level-directive被编译时, attribute-remover已经删除了低级指令的属性,因此我不必担心他们在做什么。

对于所有types的指令(不只是属性指令),可能还有一个更强大的方法,我不确定这是否可以使用内置的JQLite,但是它可以满足我们的需求。

这是另一个小想法。 只要把这个代码放在茉莉花助手(咖啡脚本)

 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 

并使用它:

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

这将完全删除给定指令的所有其他实现,从而完全访问指令的testing传递参数。 例如,mm.foundation警报指令可以被模拟:

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

然后testing:

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

很喜欢Sylvain的回答 ,我不得不把它变成一个帮手function。 大多数情况下,我需要的是取消一个子指令,这样我就可以编译和testing父容器指令而不依赖它。 所以,这个帮手让我们这样做:

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

通过这样做,您可以在注入器创build之前通过运行完全禁用一个指令:

 killDirective('myLowerLevelDirective');