Сегодня я хотел написать модульный тест для выражения часов на моем контроллере. То, что сначала казалось совершенно очевидным, оказалось довольно неприятным. В любом случае, спасибо замечательному члену сообщества 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"')
- обратите внимание на инициализацию контроллера в вашем модуле тестирования. Если вы используете контроллер в качестве синтаксиса, убедитесь, что вы установили его соответствующим образом (см. Пример раньше).