48

所以我确实看到了另一个问题:如何在指令 UT 中模拟所需的指令控制器,这基本上是我的问题,但似乎这个线程的答案是“改变你的设计”。我想确保没有办法做到这一点。我有一个指令,它声明了一个由子指令使用的控制器。我现在正在尝试为 children 指令编写 jasmine 测试,但我无法让它们在测试中编译,因为它们依赖于控制器。这是它的样子:

addressModule.directive('address', ['$http', function($http){
        return {
            replace: false,
            restrict: 'A',
            scope: {
                config: '='
            },
            template:   '<div id="addressContainer">' +
                            '<div ng-if="!showAddressSelectionPage" basic-address config="config"/>' +
                            '<div ng-if="showAddressSelectionPage" address-selector addresses="standardizedAddresses"/>' +
                        '</div>',
            controller: function($scope)
            {
                this.showAddressInput = function(){
                    $scope.showAddressSelectionPage = false;
                };

                this.showAddressSelection = function(){
                    $scope.getStandardizedAddresses();
                };

                this.finish = function(){
                    $scope.finishAddress();
                };
            },
            link: function(scope, element, attrs) {
              ...
            }
       }
}])

子指令:

addressModule.directive('basicAddress360', ['translationService', function(translationService){
        return {
            replace: true,
            restrict: 'A',
            scope: {
                config: '='
            },
            template:
                '...',
            require: "^address360",
            link: function(scope, element, attrs, addressController){
            ...
            }
       }
}])

茉莉花测试:

it("should do something", inject(function($compile, $rootScope){
            parentHtml = '<div address/>';
            subDirectiveHtml = '<div basic-address>';

            parentElement = $compile(parentHtml)(rootScope);
            parentScope = parentElement.scope();
            directiveElement = $compile(subDirectiveHtml)(parentScope);
            directiveScope = directiveElement.scope();
            $rootScope.$digest();
}));

我有没有办法用茉莉花测试子指令,如果是这样,我错过了什么?即使我可以在没有控制器功能的情况下测试指令本身,我也会很高兴。

4

3 回答 3

76

我可以想到两种方法:

1) 使用两个指令

假设我们有以下指令:

app.directive('foo', function() {
  return {
    restrict: 'E',
    controller: function($scope) {
      this.add = function(x, y) {
        return x + y;
      }
    }
  };
});

app.directive('bar', function() {
  return {
    restrict: 'E',
    require: '^foo',
    link: function(scope, element, attrs, foo) {
      scope.callFoo = function(x, y) {
        scope.sum = foo.add(x, y);
      }
    }
  };
});

为了测试该callFoo方法,您可以简单地编译这两个指令并让barusefoo的实现:

it('ensures callFoo does whatever it is supposed to', function() {
  // Arrange
  var element = $compile('<foo><bar></bar></foo>')($scope);
  var barScope = element.find('bar').scope();

  // Act
  barScope.callFoo(1, 2);

  // Assert
  expect(barScope.sum).toBe(3);
});    

工作Plunker

2) 模拟 foo 的控制器

这不是很简单,而且有点棘手。你可以element.controller()用来获取一个元素的控制器,并用 Jasmine 模拟它:

it('ensures callFoo does whatever it is supposed to', function() {
    // Arrange
    var element = $compile('<foo><bar></bar></foo>')($scope);
    var fooController = element.controller('foo');
    var barScope = element.find('bar').scope();
    spyOn(fooController, 'add').andReturn(3);

    // Act
    barScope.callFoo(1, 2);

    // Assert
    expect(barScope.sum).toBe(3);
    expect(fooController.add).toHaveBeenCalledWith(1, 2);
  });

工作Plunker

当一个指令在其link功能中立即使用另一个控制器时,就会出现棘手的部分:

app.directive('bar', function() {
  return {
    restrict: 'E',
    require: '^foo',
    link: function(scope, element, attrs, foo) {
      scope.sum = foo.add(parseInt(attrs.x), parseInt(attrs.y));
    }
  };
});

在这种情况下,您需要单独编译每个指令,以便在第二个使用它之前模拟第一个:

it('ensures callFoo does whatever it is supposed to', function() {
  // Arrange
  var fooElement = $compile('<foo></foo>')($scope);
  var fooController = fooElement.controller('foo');
  spyOn(fooController, 'add').andReturn(3);

  var barElement = angular.element('<bar x="1" y="2"></bar>')
  fooElement.append(barElement);

  // Act
  barElement = $compile(barElement)($scope);
  var barScope = barElement.scope();

  // Assert
  expect(barScope.sum).toBe(3);
  expect(fooController.add).toHaveBeenCalledWith(1, 2);
});

工作Plunker

第一种方法比第二种方法容易得多,但它依赖于第一个指令的实现,即,您不是对事物进行单元测试。另一方面,虽然模拟指令的控制器并不容易,但它可以让您更好地控制测试并消除对第一个指令的依赖。所以,明智地选择。:)

最后,我不知道有一种更简单的方法来完成上述所有操作。如果有人知道更好的方法,请改进我的答案。

于 2013-10-07T19:11:00.333 回答
57

分叉迈克尔·本福德的(奇妙的)答案。

如果你想在测试中完全隔离你的控制器/指令,你需要一种稍微不同的方法。

3)完全模拟任何所需的父控制器

当您将控制器与指令相关联时,控制器的实例将存储在元素的数据存储中。键值的命名约定是'$' + 指令名称 + 'Controller'。每当 Angular 尝试解析所需的控制器时,它都会使用此约定遍历数据层次结构以定位所需的控制器。这可以通过将模拟控制器实例插入到父元素中来轻松操作:

it('ensures callFoo does whatever it is supposed to', function() {

    // Arrange

    var fooCtrl = {
      add: function() { return 123; }
    };

    spyOn(fooCtrl, 'add').andCallThrough();

    var element = angular.element('<div><bar></bar></div>');
    element.data('$fooController', fooCtrl);

    $compile(element)($scope);

    var barScope = element.find('bar').scope();

    // Act

    barScope.callFoo(1, 2);

    // Assert

    expect(barScope.sum).toBe(123);
    expect(fooCtrl.add).toHaveBeenCalled();
});

工作的普伦克。

4) 分离链接法

在我看来,最好的方法是隔离链接方法。所有以前的方法实际上测试了太多,当情况变得比这里提供的简单示例稍微复杂一点时,它们需要太多的设置。

Angular 完美支持这种关注点分离:

// Register link function

app.factory('barLinkFn', function() {
  return function(scope, element, attrs, foo) {
    scope.callFoo = function(x, y) {
      scope.sum = foo.add(x, y);
    };
  };
});

// Register directive

app.directive('bar', function(barLinkFn) {
  return {
    restrict: 'E',
    require: '^foo',
    link: barLinkFn
  };
});

并通过更改我们的beforeEach以包含我们的链接功能...:

inject(function(_barLinkFn_) {
  barLinkFn = _barLinkFn_;
});

... 我们能做的:

it('ensures callFoo does whatever it is supposed to', function() {

  // Arrange

  var fooCtrl = {
    add: function() { return 321; }
  };

  spyOn(fooCtrl, 'add').andCallThrough();

  barLinkFn($scope, $element, $attrs, fooCtrl);

  // Act

  $scope.callFoo(1, 2);

  // Assert

  expect($scope.sum).toBe(321);
  expect(fooCtrl.add).toHaveBeenCalled();

});

工作的普伦克。

这样我们只测试相关的东西,如果需要,可以使用相同的方法来隔离编译功能。

于 2013-11-13T10:20:20.787 回答
9

5)注入指令定义并模拟控制器的功能

另一种方法是注入指令的定义并模拟我们需要的任何东西。最好的一点是,您可以完全为您的 children 指令编写单元测试,而无需依赖您的父母。

使用 inject() 您可以注入任何提供指令名称 + 'Directive' 的指令定义,然后访问其方法并根据需要替换它们

it('ensures callFoo does whatever it is supposed to', inject(function(fooDirective) {
  var fooDirectiveDefinition = fooDirective[0];

  // Remove any behavior attached to original link function because unit
  // tests should isolate from other components
  fooDirectiveDefinition.link = angular.noop;

  // Create a spy for foo.add function
  var fooAddMock = jasmine.createSpy('add');

  // And replace the original controller with the new one defining the spy
  fooDirectiveDefinition.controller = function() {
    this.add = fooAddMock;
  };

  // Arrange
  var element = $compile('<foo><bar></bar></foo>')($scope);
  var barScope = element.find('bar').scope();

  // Act
  barScope.callFoo(1, 2);

  // Verify that add mock was called with proper parameters
  expect(fooAddMock).toHaveBeenCalledWith(1, 2);
}));

这个想法是由AngularJS Google Group中的Daniel Tabuenca提出的

在这个Plunker Daniel 模拟了 ngModel 指令

于 2014-04-14T15:39:40.603 回答