在我们的应用程序中,我们有几层嵌套指令。我正在尝试为顶级指令编写一些单元测试。我已经嘲笑了指令本身需要的东西,但现在我遇到了来自低级指令的错误。在我对顶级指令的单元测试中,我不想担心低级指令中发生了什么。我只是想模仿较低级别的指令,基本上它没有做任何事情,所以我可以单独测试顶级指令。
我尝试通过这样的方式覆盖指令定义:
angular.module("myModule").directive("myLowerLevelDirective", function() {
return {
link: function(scope, element, attrs) {
//do nothing
}
}
});
然而,这并没有覆盖它,除了真正的指令之外它只是运行它。如何阻止这些低级指令在顶级指令的单元测试中做任何事情?
答案 0 :(得分:79)
指令只是工厂,因此最好的方法是使用module
函数模拟指令的工厂,通常在beforeEach
块中。假设你有一个名为do-something的指令,这个指令被一个名为do-something-else的指令使用,你就是这样嘲笑它:
beforeEach(module('yourapp/test', function($provide){
$provide.factory('doSomethingDirective', function(){ return {}; });
}));
// Or using the shorthand sytax
beforeEach(module('yourapp/test', { doSomethingDirective: {} ));
然后在测试中编译模板时将覆盖该指令
inject(function($compile, $rootScope){
$compile('<do-something-else></do-something-else>', $rootScope.$new());
});
请注意,您需要在名称中添加“Directive”后缀,因为编译器会在内部执行此操作:https://github.com/angular/angular.js/blob/821ed310a75719765448e8b15e3a56f0389107a5/src/ng/compile.js#L530
答案 1 :(得分:65)
模仿指令的简洁方法是使用$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;
});
}));
你必须确保mock比你正在模拟的指令具有更高的优先级,并且mock是终端,这样原始指令就不会被编译。
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);
});
});
还有一些事情:
创建模拟时,您必须考虑是否需要replace:true
和/或template
。例如,如果您模拟ng-src
以阻止对后端的调用,那么您不希望replace:true
并且您不想指定template
。但如果你嘲笑某些视觉效果,你可能想要。
如果将优先级设置为100以上,则不会插入mocks的属性。见$compile source code。例如,如果您模拟ng-src
并设置priority:101
,那么您最终会在模拟中使用ng-src="{{variable}}"
而不是ng-src="interpolated-value"
。
Here is a plunker包含一切。感谢@trodrigues指出我正确的方向。
Here is some doc解释了更多内容,请查看“配置块”部分。感谢@ebelanger!
答案 2 :(得分:29)
由于指令注册的实现,似乎不可能用模拟的指令替换现有的指令。
但是,您可以使用多种方法对较高级别指令进行单元测试,而不受低级指令的干扰:
1)请勿在单元测试模板中使用低级指令:
如果您的更高级别指令未添加您的低级指令,则在您的单元测试中使用仅包含高级指令的模板:
var html = "<div my-higher-level-directive></div>";
$compile(html)(scope);
因此,较低级别的指令不会干扰。
2)在指令实现中使用服务:
您可以通过服务提供较低级别的指令链接功能:
angular.module("myModule").directive("myLowerLevelDirective", function(myService) {
return {
link: myService.lowerLevelDirectiveLinkingFunction
}
});
然后,您可以在单元测试中模拟此服务,以避免干扰您的更高级别指令。如果需要,该服务甚至可以提供整个指令对象。
3)您可以使用终端指令:
覆盖您的低级指令angular.module("myModule").directive("myLowerLevelDirective", function(myService) {
return {
priority: 100000,
terminal: true,
link: function() {
// do nothing
}
}
});
使用终端选项和更高的优先级,不会执行真正的低级指令。 directive doc中有更多信息。
在此Plunker中了解它的工作原理。
答案 3 :(得分:5)
您可以修改$templateCache
中的模板以删除任何较低级别的指令:
beforeEach(angular.mock.inject(function ($templateCache) {
$templateCache.put('path/to/template.html', '<div></div>');
}));
答案 4 :(得分:3)
我被迫更多地考虑这个问题,我想出了一个满足我们需求的解决方案。我们所有的指令都是属性,因此我创建了一个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);
});
}
}
});
然后我正在测试的指令的html看起来像这样:
<div my-higher-level-directive attribute-remover="my-lower-level-directive,another-loweler-level-directive"></div>
因此,当编译my-higher-level-directive
时,attribute-remover
已经删除了较低级别指令的属性,因此我不必担心它们正在做什么。
对于所有类型的指令(不仅仅是属性指令),可能有更强大的方法来执行此操作,并且我不确定如果仅使用内置的JQLite,它是否有效,但它适用于我们需要的内容。 / p>
答案 5 :(得分:3)
喜欢Sylvain's answer,我不得不把它变成一个辅助函数。通常,我需要的是杀死子指令,以便我可以编译和测试父容器指令而不依赖它。所以,这个助手让我们这样做:
function killDirective(directiveName) {
angular.mock.module(function($compileProvider) {
$compileProvider.directive(directiveName, function() {
return {
priority: 9999999,
terminal: true
}
});
});
}
有了这个,您可以通过在创建注入器之前运行此来完全禁用指令:
killDirective('myLowerLevelDirective');
答案 6 :(得分:1)
这是另一个小想法。只需将此代码放入茉莉花助手(咖啡脚本)
即可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) ->
这将完全删除给定指令的所有其他实现,从而完全访问测试传递给指令的参数。举个例子,mm.foundation alert指令可以用:
来模拟beforeEach mockDirective 'alert', ->
scope:
type: '='
然后测试:
expect(element.find('alert').data('$isolateScopeNoTemplate').type).toEqual