测试指令需要控制器

时间:2013-10-07 14:08:16

标签: angularjs jasmine

所以我确实看到了另一个问题:How to mock required directive controller in directive UT这基本上是我的问题,但似乎这个帖子的答案是“改变你的设计”。我想确保没有办法做到这一点。我有一个指令声明了一个由children指令使用的控制器。我现在正在尝试为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();
}));

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

3 个答案:

答案 0 :(得分:74)

我可以想到两种方法:

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方法,您可以简单地编译这两个指令,并让bar使用foo的实现:

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

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

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

Working Plunker

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

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

答案 1 :(得分:56)

分享Michael Benford的(梦幻般的)答案。

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

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

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

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

Working Plunker.

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

});

Working Plunker.

这样我们只测试所关注的事情,如果需要,可以使用相同的方法来隔离编译函数。

答案 2 :(得分:9)

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

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

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

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

这个想法是由Daniel Tabuenca

中的AngularJS Google Group提出的

在这个Plunker Daniel模拟了ngModel指令