50

在我们的应用程序中,我们有几层嵌套指令。我正在尝试为顶级指令编写一些单元测试。我已经嘲笑了指令本身需要的东西,但现在我遇到了来自较低级别指令的错误。在我对顶级指令的单元测试中,我不想担心低级指令中发生了什么。我只想模拟较低级别的指令,基本上让它什么都不做,所以我可以单独测试顶级指令。

我尝试通过执行以下操作来覆盖指令定义:

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

但是,这不会覆盖它,它只是在真正的指令之外运行它。如何阻止这些较低级别的指令在我的顶级指令的单元测试中执行任何操作?

4

7 回答 7

82

指令只是工厂,因此最好的方法是在使用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());
});

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

于 2014-01-06T13:30:55.387 回答
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;
  });
}));

您必须确保模拟比您正在模拟的指令具有更高的优先级,并且模拟是终端的,这样原始指令就不会被编译。

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 以上,则不会插入模拟的属性。请参阅$compile 源代码。例如,如果你 mockng-src和 set priority:101,那么你最终会得到ng-src="{{variable}}"not ng-src="interpolated-value"on your mock。

这是一个什么都有的笨蛋。感谢@trodrigues 为我指明了正确的方向。

这是一些解释更多的文档,请查看“配置块”部分。感谢@ebelanger!

于 2014-02-21T01:35:44.293 回答
32

由于指令注册的实施,似乎不可能用模拟指令替换现有指令。

但是,您有几种方法可以对更高级别的指令进行单元测试,而不会受到较低级别指令的干扰:

1)不要在单元测试模板中使用较低级别的指令:

如果您的较高级别指令未添加您的较低级别指令,则在您的单元测试中使用仅包含您 higer-level-directive 的模板:

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

使用终端选项和更高的优先级,您的真正较低级别的指令将不会被执行。指令文档中的更多信息。

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

于 2013-07-19T16:27:01.317 回答
5

您可以在内部修改模板$templateCache以删除任何较低级别的指令:

beforeEach(angular.mock.inject(function ($templateCache) {
  $templateCache.put('path/to/template.html', '<div></div>');
}));
于 2013-12-27T13:33:37.560 回答
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,我不确定这是否有效,但它可以满足我们的需要。

于 2013-07-19T16:10:16.640 回答
3

非常喜欢Sylvain 的回答,我不得不把它变成一个辅助函数。大多数情况下,我需要杀死一个子指令,这样我就可以编译和测试没有依赖关系的父容器指令。所以,这个助手让我们这样做:

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

这样,您可以通过在创建注入器之前运行它来完全禁用指令:

killDirective('myLowerLevelDirective');
于 2016-03-31T16:55:05.293 回答
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 警报指令可以模拟为:

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

然后测试:

expect(element.find('alert').data('$isolateScopeNoTemplate').type).toEqual 
于 2015-08-07T10:00:43.557 回答