Подтвердить что ты не робот

Тестирование директив, требующих контроллеров

Итак, я еще раз задал вопрос: Как издеваться над требуемым директивным контроллером в директиве UT, который в основном является моей проблемой, но кажется, что ответ на этот вопрос был "изменил ваш дизайн." Я хотел убедиться, что это невозможно. У меня есть директива, которая объявляет контроллер, который используется директивами детей. Теперь я пытаюсь написать тесты жасмина для директивы children, но я не могу их компилировать в тестах, потому что они зависят от контроллера. Вот как это выглядит:

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

Нет ли способа проверить суб-директиву с жасмином, и если да, то что мне не хватает? Даже если бы я мог проверить сама директива без функций контроллера, я был бы счастлив.

4b9b3361

Ответ 1

Я могу представить два подхода:

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

Рабочий Plunker.

2) Контроллер mock 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.

Первый подход проще, чем второй, но он основан на реализации первой директивы, т.е. вы не проверяете объекты. С другой стороны, хотя издеваться над директивным контроллером не так просто, он дает вам больше контроля над тестом и устраняет зависимость от первой директивы. Итак, выбирайте разумно.:)

Наконец, я не знаю более простого способа сделать все это. Если кто-то знает о лучшем подходе, пожалуйста, улучшите мой ответ.

Ответ 2

Увлечение (фантастическим) ответом Майкла Бенфорда.

Если вы хотите полностью изолировать свой контроллер/директиву в своем тесте, вам понадобится немного другой подход.

3) Полностью издевается над любым необходимым родительским контроллером

Когда вы связываете контроллер с директивой, экземпляр контроллера хранится в хранилище данных элемента. Соглашение об именовании для ключевого значения - "$" + название директивы + "Контроллер". Всякий раз, когда 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();
});

Рабочий 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();

});

Рабочий плункер.

Таким образом мы проверяем только те вещи, которые могут быть использованы для того, чтобы изолировать функцию компиляции при необходимости.

Ответ 3

5) Внедрение определения директивы и издевательство над функцией контроллера

Другой подход - ввести определение директивы и высмеять все, что нам нужно. Самое лучшее в этом состоит в том, что вы можете полностью написать модульные тесты для вашей директивы для детей, в зависимости от ваших родителей.

Используя команду inject(), вы можете ввести любое определение директив, указав название директивы + 'Директива', а затем получить доступ к его методам и заменить их по мере необходимости

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 Даниэль издевается над директивой ngModel