如何模拟指令以启用更高级指令的单元测试?

时间:2013-07-08 17:59:28

标签: unit-testing angularjs angularjs-directive

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

我尝试通过这样的方式覆盖指令定义:

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

然而,这并没有覆盖它,除了真正的指令之外它只是运行它。如何阻止这些低级指令在顶级指令的单元测试中做任何事情?

7 个答案:

答案 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