你如何模拟指令来启用更高级指令的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');