BindToController в модульных тестах
Я использую bindToController в директиве, чтобы иметь изолированную область, непосредственно прикрепленную к контроллеру, например:
app.directive('xx', function () {
return {
bindToController: true,
controller: 'xxCtrl',
scope: {
label: '@',
},
};
});
Тогда в контроллере у меня есть значение по умолчанию, если метка в HTML не указана:
app.controller('xxCtrl', function () {
var ctrl = this;
ctrl.label = ctrl.label || 'default value';
});
Как я могу создать экземпляр xxCtrl в модульных тестах Jasmine, чтобы я мог проверить ctrl.label?
describe('buttons.RemoveButtonCtrl', function () {
var ctrl;
beforeEach(inject(function ($controller) {
// What do I do here to set ctrl.label BEFORE the controller runs?
ctrl = $controller('xxCtrl');
}));
it('should have a label', function () {
expect(ctrl.label).toBe('foo');
});
});
Отметьте этот, чтобы проверить проблему.
Ответы
Ответ 1
В Angular 1.3 (см. ниже для 1.4 +)
Копаясь в исходном коде AngularJS, я нашел недокументированный третий аргумент службе $controller
, называемой later
(см. $controller source).
Если true, $controller()
возвращает функцию с свойством instance
, на котором вы можете установить свойства.
Когда вы будете готовы создать экземпляр контроллера, вызовите функцию, и он будет создавать экземпляр контроллера со свойствами, доступными в конструкторе.
Ваш пример будет работать следующим образом:
describe('buttons.RemoveButtonCtrl', function () {
var ctrlFn, ctrl, $scope;
beforeEach(inject(function ($rootScope, $controller) {
scope = $rootScope.$new();
ctrlFn = $controller('xxCtrl', {
$scope: scope,
}, true);
}));
it('should have a label', function () {
ctrlFn.instance.label = 'foo'; // set the value
// create controller instance
ctrl = ctrlFn();
// test
expect(ctrl.label).toBe('foo');
});
});
Здесь обновленный Plunker (пришлось обновить Angular, чтобы он работал, теперь это 1.3.0-rc.4): http://plnkr.co/edit/tnLIyzZHKqPO6Tekd804?p=preview
Обратите внимание, что, вероятно, не рекомендуется использовать его, чтобы процитировать исходный код Angular:
Мгновенный запуск контроллера: этот механизм используется для создания экземпляр объекта перед вызовом конструктора контроллера сам по себе.
Это позволяет добавлять свойства к контроллеру до вызывается конструктор. В первую очередь это используется для выделения области привязки в компиляции $.
Эта функция не предназначена для использования приложениями и, следовательно, не является задокументировано публично.
Однако отсутствие механизма тестирования контроллеров с bindToController: true
заставило меня использовать его, тем не менее. Возможно, ребятам из Angular следует подумать о том, чтобы сделать этот флаг общедоступным.
Под капотом он использует временный конструктор, мы также можем написать его сами, я думаю.
Преимущество вашего решения состоит в том, что конструктор не вызывается дважды, что может вызвать проблемы, если свойства не имеют значений по умолчанию, как в вашем примере.
Angular 1.4+ (обновление 2015-12-06):
Команда Angular добавила прямую поддержку для этого в версии 1.4.0. (См. # 9425)
Вы можете просто передать объект функции $controller
:
describe('buttons.RemoveButtonCtrl', function () {
var ctrl, $scope;
beforeEach(inject(function ($rootScope, $controller) {
scope = $rootScope.$new();
ctrl = $controller('xxCtrl', {
$scope: scope,
}, {
label: 'foo'
});
}));
it('should have a label', function () {
expect(ctrl.label).toBe('foo');
});
});
Смотрите также этот пост в блоге.
Ответ 2
Тестирование устройств BindToController с использованием ES6
Если вы используете ES6, вы можете напрямую импортировать контроллер и протестировать, не используя angular mocks.
Директива
import xxCtrl from './xxCtrl';
class xxDirective {
constructor() {
this.bindToController = true;
this.controller = xxCtrl;
this.scope = {
label: '@'
}
}
}
app.directive('xx', new xxDirective());
Контроллер:
class xxCtrl {
constructor() {
this.label = this.label || 'default value';
}
}
export default xxCtrl;
Тест контроллера:
import xxCtrl from '../xxCtrl';
describe('buttons.RemoveButtonCtrl', function () {
let ctrl;
beforeEach(() => {
xxCtrl.prototype.label = 'foo';
ctrl = new xxCtrl(stubScope);
});
it('should have a label', () => {
expect(ctrl.label).toBe('foo');
});
});
см. это для получения дополнительной информации:
Надлежащее модульное тестирование
Angular JS
приложения с модулями ES6
Ответ 3
На мой взгляд, этот контроллер не предназначен для тестирования изолированно, потому что он никогда не будет работать изолированно:
app.controller('xxCtrl', function () {
var ctrl = this;
// where on earth ctrl.lable comes from???
ctrl.newLabel = ctrl.label || 'default value';
});
Он тесно связан с директивой, полагающейся на получение ее свойств области. Он не может использоваться повторно. От взгляда на этот контроллер, я должен задаться вопросом, откуда эта переменная. Это не лучше, чем негерметичная функция, использующая переменную из внешней области:
function Leaky () {
... many lines of code here ...
// if we are here we are too tired to notice the leakyVariable:
importantData = process(leakyVariable);
... mode code here ...
return unpredictableResult;
}
Теперь у меня есть нечеткая функция, поведение которой очень непредсказуемо на основе переменной leakyVariable
присутствует (или нет) в любой области, вызываемой функцией.
Неудивительно, что эта функция - это кошмар для тестирования. Это на самом деле хорошая вещь, возможно, чтобы заставить разработчика переписать функцию на нечто более модульное и повторно используемое. Что не так сложно:
function Modular (outsideVariable) {
... many lines of code here ...
// no need to hit our heads against the wall to wonder where the variable comes from:
importantData = process(outsideVariable);
... mode code here ...
return predictableResult;
}
Отсутствие проблем с утечкой и очень простое тестирование и повторное использование. Который мне говорит, что использование старого старого $scope
- лучший способ:
app.controller('xxCtrl', function ($scope) {
$scope.newLabel = $scope.label || 'default value';
});
Простой, короткий и простой в тестировании. Кроме того, нет большого количества объектов с объективом.
Первоначальная аргументация синтаксиса controllerAs
- это нечеткая область, унаследованная от родителя. Однако директивная изолированная область уже решает эту проблему. Таким образом, я не вижу причин использовать более сильный синтаксис утечки.
Ответ 4
Я нашел способ, который не особенно элегантен, но работает как минимум (если есть лучший вариант, оставляйте комментарий).
Мы устанавливаем значение, которое "приходит" из директивы, а затем снова вызываем функцию контроллера, чтобы проверить, что она делает. Я сделал помощника "invokeController" более сухим.
Например:
describe('buttons.RemoveButtonCtrl', function () {
var ctrl, $scope;
beforeEach(inject(function ($rootScope, $controller) {
scope = $rootScope.$new();
ctrl = $controller('xxCtrl', {
$scope: scope,
});
}));
it('should have a label', function () {
ctrl.label = 'foo'; // set the value
// call the controller again with all the injected dependencies
invokeController(ctrl, {
$scope: scope,
});
// test whatever you want
expect(ctrl.label).toBe('foo');
});
});
beforeEach(inject(function ($injector) {
window.invokeController = function (ctrl, locals) {
locals = locals || {};
$injector.invoke(ctrl.constructor, ctrl, locals);
};
}));