Сегодня я хотел написать модульный тест для выражения часов на моем контроллере. То, что сначала казалось совершенно очевидным, оказалось довольно неприятным. В любом случае, спасибо замечательному члену сообщества Angular IRC, я смог быстро решить эту проблему. Итак, вот история.
Эта статья является частью моей серии «Learning NG», в которой представлены некоторые мои приключения во время изучения Angular. Проверьте вступление серии и другие статьи . Обратите внимание, я новичок в Angular, поэтому я более чем рад любым отзывам и предложениям по улучшению от более опытных людей, чем я.
Фон
Новые лучшие практики Angular предлагают использовать — то, что они называют — «контроллер как» синтаксис. Таким образом, вместо записи контроллера, как
module.controller('MainCtrl', function($scope){
$scope.someScopeVariable = 'Hello, world!';
});
Вы должны вместо этого написать это так.
module.controller('MainCtrl', function(){
var vm = this; // this is a best practice approach
vm.someScopeVariable = 'Hello, world!';
});
На стороне HTML вы обычно включаете контроллер, используя подобный синтаксис.
<div ng-controller="MainCtrl as vm">
{{ vm.someScopeVariable }}
</div>
проблема
Теперь рассмотрим, что у нас есть определенное выражение наблюдения в контроллере, которое мы хотели бы проверить.
app.controller('MainCtrl', function($scope) {
var vm = this;
var previousSelection = null;
vm.currentSelection = null;
$scope.$watch('vm.currentSelection', function(newVal, oldVal){
// we'd like to test THIS LINE HERE
previousSelection = oldVal;
});
vm.changeSelection = function(shouldRevert){
if(shouldRevert){
vm.currentSelection = previousSelection;
}
};
});
Обратите внимание, что я делаю инъекцию, из-за $scope которой может показаться, что я использую синтаксис контроллера $ scope. На самом деле это для возможности регистрации $watch.
Кроме того, выше приведен простой демонстрационный пример, который, слегка измененный, может быть полезен, например, для отмены выбора пользователя в раскрывающемся меню ng-change.
В любом случае, если мы хотим, чтобы вышесказанное, мы могли бы написать следующий тестовый сценарий.
describe('Testing $watch expressions', function() {
var $scope = null;
var ctrl = null;
//you need to indicate your module in a test
beforeEach(module('plunker'));
describe('using the controller as syntax', function() {
beforeEach(inject(function($rootScope, $controller) {
$scope = $rootScope.$new();
ctrl = $controller('MainCtrl', {
$scope: $scope
});
}));
it('test using $digest', function() {
// make an initial selection
ctrl.currentSelection = 'Hi';
$scope.$digest();
// make another one
ctrl.currentSelection = 'New';
$scope.$digest();
// simulate a ng-change which should revert to the previous value
ctrl.changeSelection(true);
expect(ctrl.currentSelection).toEqual('Hi');
});
});
});
Обратите внимание, что я использую $scope.$digest() после установки currentSelectionна контроллере. Это необходимо для запуска «цикла дайджеста», который вызывает $watch выражение, которое я определил. К сожалению, это не работает! Выражение watch вызывается, но newVal и oldVal оба не определены.
Вместо этого, если я вернул свой контроллер к «старому» синтаксису $ scope …
app.controller('MainCtrl', function($scope) {
var previousSelection = null;
$scope.currentSelection = null;
$scope.$watch('currentSelection', function(newVal, oldVal){
previousSelection = oldVal;
});
$scope.changeSelection = function(shouldRevert){
if(shouldRevert){
$scope.currentSelection = previousSelection;
}
};
});
… и скорректировал мои тесты соответственно:
it('test using $digest', function() {
// make an initial selection
$scope.currentSelection = 'Hi';
$scope.$digest();
// make another one
$scope.currentSelection = 'New';
$scope.$digest();
// simulate a ng-change which should revert to the previous value
$scope.changeSelection(true);
expect($scope.currentSelection).toEqual('Hi');
});
..то было вызвано выражение $ watch с правильным значением, и тесты прошли, как и ожидалось.
В качестве альтернативы, я мог бы оставить синтаксис «контроллер как» ранее, и вместо вызова $scope.$digest() в моих тестах, вызовите $scope.$apply('...'):
it('test using $scope.$apply(...)', function() {
// make an initial selection
$scope.$apply('vm.currentSelection="Hi"');
// make another one
$scope.$apply('vm.currentSelection="New"');
// simulate a ng-change which should revert to the previous value
ctrl.changeSelection(true);
expect(ctrl.currentSelection).toEqual('Hi');
});
Это тоже сработало. Что тут не так ??
Я разместил на канале IRC ..
[14:32:52] Interesting, when unit testing $watch expressions it makes a difference whether you used the "controller as" syntax or not. http://plnkr.co/edit/MVOgfmXVG1MzUg6nfM6W?p=preview [14:34:01] of course - watch expressions watch on the scope. [14:36:46] sacho: yep, but by executing $scope.$digest() in the tests I'd expect that the $watch expression is executed...which, btw it is, but not with the correct values [14:37:17] sacho: Instead, it seems that in that case you have to do something like $scope.$apply('someScopeVar = "some new value"'); [14:37:26] then it fires as well, but with the passed new value [14:37:35] that's kinda odd.. [14:38:09] while, when using the $scope syntax, I can simply call $scope.$digest() and everything works as expected... [14:38:13] huh? ... [14:46:08] juristr, well, you're not placing the controller on the scope, anywhere. [14:46:26] so you're not using controllerAs.
Ой..! Проблема в beforeEach. Пока я предполагал, что следующие строки подключают контроллер к $scope
beforeEach(inject(function($rootScope, $controller) {
$scope = $rootScope.$new();
ctrl = $controller('MainCtrl', {
$scope: $scope
});
}));
… что они делают … но контроллер / область не привязаны к vmсвойству, которое $watch ожидает выражение …
Таким образом, переходя на …
beforeEach(inject(function($rootScope, $controller) {
$scope = $rootScope.$new();
ctrl = $controller('MainCtrl', {
$scope: $scope
});
// THIS was missing
$scope.vm = ctrl;
}));
.. делает все работает как положено, даже при использовании $scope.$digest().
Вы можете поиграть с этим самостоятельно в этом Plunkr.
Вывод
На самом деле это довольно сложно и легко ошибиться, особенно когда вы смотрите на тестовые примеры, которые работают с кодом, который использует несколько более старый «синтаксис области видимости». Я еще не уверен, что обернул голову вокруг этой проблемы еще … если у меня будет лучшее объяснение, я обновлю почту …
Подвести итоги:
- использование
$scope.$apply('theScopeVariable = "new value"') - обратите внимание на инициализацию контроллера в вашем модуле тестирования. Если вы используете контроллер в качестве синтаксиса, убедитесь, что вы установили его соответствующим образом (см. Пример раньше).