Angularjs unit test watch callback not getting called even with scope.$digest
angularjs, angularjs-scope, unit-testing, watch
Solution
I just ran into this problem within my own code base and the answer turned out to be that I needed a scope.$digest() call right after instantiating the controller. Within your tests you have to call scope.$digest() manually after each change in watched variables. This includes after the controller is constructed to record the initial watched value(s).
In addition, as Vitall specified in the comments, you need $watchCollection() for collections.
Changing this watch in your simple example resolves the issue:
$scope.$watch('subject_list', function(newValue, oldValue){
if(newValue !== oldValue) {
console.log('testing');
$scope.test = 'test';
}
});
to:
$scope.$watchCollection('subject_list', function(newValue, oldValue){
if(newValue !== oldValue) {
console.log('testing');
$scope.test = 'test';
}
});
I made a jsfiddle to demonstrate the difference:
http://jsfiddle.net/ydv8k4zy/ - original with failing test - fails due to using $watch, not $watchCollection
http://jsfiddle.net/ydv8k4zy/1/ - functional version
http://jsfiddle.net/ydv8k4zy/2/ - $watchCollection fails without the initial scope.$digest.
If you play around With console.log on the second failing item you'll see that the watch is called with the same value for old and new after the scope.$digest() (line 25).
Problem
I'm struggling unit testing a controller that watches a couple variables. In my unit tests, I can't get the callback for the $watch function to be called, even when calling scope.$digest(). Seems like this should be pretty simple, but I'm having no luck. Here's what I have in my controller: ``` angular.module('app') .controller('ClassroomsCtrl', function ($scope, Classrooms) { $scope.subject_list = []; $scope.$watch('subject_list', function(newValue, oldValue){ if(newValue !== oldValue) { $scope.classrooms = Classrooms.search(ctrl.functions.search_params()); } }); }); ``` And here's my unit test: ``` angular.module('MockFactories',[]). factory('Classrooms', function(){ return jasmine.createSpyObj('ClassroomsStub', [ 'get','save','query','remove','delete','search', 'subjects', 'add_subject', 'remove_subject', 'set_subjects' ]); }); describe('Controller: ClassroomsCtrl', function () { var scope, Classrooms, controllerFactory, ctrl; function createController() { return controllerFactory('ClassroomsCtrl', { $scope: scope, Classrooms: Classrooms }); } // load the controller's module beforeEach(module('app')); beforeEach(module('MockFactories')); beforeEach(inject(function($controller, $rootScope, _Classrooms_ ){ scope = $rootScope.$new(); Classrooms = _Classrooms_; controllerFactory = $controller; ctrl = createController(); })); describe('Scope: classrooms', function(){ beforeEach(function(){ Classrooms.search.reset(); }); it('should call Classrooms.search when scope.subject_list changes', function(){ scope.$digest(); scope.subject_list.push(1); scope.$digest(); expect(Classrooms.search).toHaveBeenCalled(); }); }); }); ``` I've tried replacing all the scope.$digest() calls with scope.$apply() calls. I've tried calling them 3 or 4 times, but I can't get the callback of the $watch to get called. Any thoughts as to what could be going on here? UPDATE: Here's an even simpler example, that doesn't deal with mocks, stubbing or injecting factories. ``` angular.module('edumatcherApp') .controller('ClassroomsCtrl', function ($scope) { $scope.subject_list = []; $scope.$watch('subject_list', function(newValue, oldValue){ if(newValue !== oldValue) { console.log('testing'); $scope.test = 'test'; } }); ``` And unit test: ``` it('should set scope.test', function(){ scope.$digest(); scope.subject_list.push(1); scope.$digest(); expect(scope.test).toBeDefined(); }); ``` This fails too with "Expected undefined to be defined." and nothing is logged to the console. UPDATE 2 2 more interesting things I noted. It seems like one problem is that newValue and oldValue are the same when the callback is called. I logged both to the console, and they are both equal to []. So, for example, if I change my $watch function to look like this: ``` $scope.$watch('subject_list', function(newValue, oldValue){ console.log('testing'); $scope.test = 'test'; }); ``` the test passes fine. Not sure why newValue and oldValue aren't getting set correctly. If I change my $watch callback function to be a named function, and just check to see if the named function is ever called, that fails as well. For example, I can change my controller to this: ``` $scope.update_classrooms = function(newValue, oldValue){ $scope.test = 'testing'; console.log('test'); }; $scope.watch('subject_list', $scope.update_classrooms); ``` And change my test to this: ``` it('should call update_classrooms', function(){ spyOn(scope,'update_classrooms').andCallThrough(); scope.$digest(); scope.subject_list.push(1); scope.$digest(); expect(scope.update_classrooms).toHaveBeenCalled(); }); ``` it fails with "Expected spy update_classrooms to have been called." In this case, update_classrooms is definitely getting called, because 'test' gets logged to the console. I'm baffled.