Контроллеры управления модулем тестирования в Angular без глобального контроллера

В превосходном репозитории Vojta Jina, где он демонстрирует тестирование директив, он определяет директивный контроллер вне оболочки модуля. Глянь сюда: https://github.com/vojtajina/ng-directive-testing/blob/master/js/tabs.js

Разве это не плохая практика и не загрязняет глобальное пространство имен?

Если бы у кого-то было другое место, где было бы логично вызвать что-то TabsController, разве это не перерыв?

Тесты для указанной директивы можно найти здесь: https://github.com/vojtajina/ng-directive-testing/commit/test-controller

Можно ли тестировать контроллеры директив отдельно от остальной части директивы, не помещая контроллер в глобальное пространство имен?

Было бы неплохо инкапсулировать всю директиву в определение app.directive(...).

Ответ 1

Отличный вопрос!

Итак, это общая проблема не только с контроллерами, но и потенциально с услугами, которые могут потребоваться для директивы, чтобы выполнять свою работу, но не обязательно хотят подвергать этот контроллер/службу "внешнему миру".

Я твердо верю, что глобальные данные являются злыми, и их следует избегать, и это также относится к директивным контроллерам. Если принять это предположение, мы можем использовать несколько разных подходов для определения этих контроллеров "локально". При этом нам нужно иметь в виду, что контроллер должен быть "легко" доступен для модульных тестов, поэтому мы не можем просто скрыть его в директивное закрытие. Возможности IMO:

1) Во-первых, мы могли бы просто определить директивный контроллер на уровне модуля, ex::

angular.module('ui.bootstrap.tabs', [])
  .controller('TabsController', ['$scope', '$element', function($scope, $element) {
    ...
  }])
 .directive('tabs', function() {
  return {
    restrict: 'EA',
    transclude: true,
    scope: {},
    controller: 'TabsController',
    templateUrl: 'template/tabs/tabs.html',
    replace: true
  };
})

Это простой метод, который мы используем в https://github.com/angular-ui/bootstrap/blob/master/src/tabs/tabs.js, который основан на работе Vojta.

Хотя это очень простой метод, следует отметить, что контроллер по-прежнему подвергается воздействию всего приложения, что означает, что другой модуль может потенциально переопределить его. В этом смысле контроллер локализует приложение AngularJS (поэтому не загрязняет глобальную область окна), но также глобально для всех модулей AngularJS.

2) Используйте область закрытия и специальные файлы для тестирования.

Если мы хотим полностью скрыть функцию контроллера, мы можем обернуть код в закрытии. Это метод, который использует AngularJS. Например, глядя на NgModelController, мы видим, что он определяется как "глобальная" функция в своих собственных файлах (и, следовательно, легко доступен для тестирования), но весь файл закрывается в течение времени сборки:

Подводя итог: опция (2) является "более безопасной", но для сборки требуется немного предварительная настройка.

Ответ 2

Я предпочитаю время от времени включать мой контроллер вместе с директивой, поэтому мне нужен способ проверить это.

Сначала директива

angular.module('myApp', [])
  .directive('myDirective', function() {
    return {
      restrict: 'EA',
      scope: {},
      controller: function ($scope) {
        $scope.isInitialized = true
      },
      template: '<div>{{isInitialized}}</div>'
    }
})

Затем тесты:

describe("myDirective", function() {
  var el, scope, controller;

  beforeEach inject(function($compile, $rootScope) {
    # Instantiate directive.
    # gotacha: Controller and link functions will execute.
    el = angular.element("<my-directive></my-directive>")
    $compile(el)($rootScope.$new())
    $rootScope.$digest()

    # Grab controller instance
    controller = el.controller("myDirective")

    # Grab scope. Depends on type of scope.
    # See angular.element documentation.
    scope = el.isolateScope() || el.scope()
  })

  it("should do something to the scope", function() {
    expect(scope.isInitialized).toBeDefined()
  })
})

См. angular.элементную документацию для получения дополнительных способов получения данных из созданной вами директивы.

Остерегайтесь того, что создание экземпляра директивы подразумевает, что контроллер и все функции ссылок уже запущены, что может повлиять на ваши тесты.

Ответ 3

Метод Джеймса работает для меня. Один маленький поворот, хотя, если у вас есть внешний шаблон, вам нужно будет вызвать $httpBackend.flush() до $rootScope. $Digest(), чтобы позволить angular выполнить ваш контроллер.

Я думаю, это не должно быть проблемой, если вы используете https://github.com/karma-runner/karma-ng-html2js-preprocessor

Ответ 4

Есть ли что-то не так с этим? Кажется предпочтительным, так как вы избегаете размещения своего контроллера в глобальном пространстве имен и можете протестировать то, что хотите (т.е. Контроллер), без необходимости компилировать html $.

Пример определения директивы:

 .directive('tabs', function() {
  return {
    restrict: 'EA',
    transclude: true,
    scope: {},
    controller: function($scope, $attrs) {
      this.someExposedMethod = function() {};
    },
    templateUrl: 'template/tabs/tabs.html',
    replace: true
  };

Затем в вашем тесте Жасмин попросите директиву, которую вы создали, с помощью "name + Directive" (например, "tabsDirective" ):

var tabsDirective = $injector.get('tabsDirective')[0];
// instantiate and override locals with mocked test data
var tabsDirectiveController = $injector.instantiate(tabsDirective.controller, {
  $scope: {...}
  $attrs: {...}
});

Теперь вы можете протестировать методы контроллера:

expect(typeof tabsDirectiveController.someExposedMethod).toBe('function');

Ответ 5

Используйте IIFE, который является общей методикой, позволяющей избежать конфликта глобального пространства имен, а также сэкономить сложную встроенную гимнастику, а также обеспечить свободу в своем объеме.

 (function(){

  angular.module('app').directive('myDirective', function(){
     return {
       .............
       controller : MyDirectiveController,
       .............
     }
  });

  MyDirectiveController.$inject = ['$scope'];

  function MyDirectiveController ($scope) {

  }

})();