Контроллеры управления модулем тестирования в 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) {
}
})();