Stuart Stuart - 8 days ago 4
AngularJS Question

Angular unit testing, what's causing the $rootScope.$watch() to fire?

This is my Controller.

(function() {
'use strict';

app.controller('NavController', navController);

function navController($rootScope, $scope, $location, $window, NavFactory) {

$scope.menuConfig = null;
$scope.menuItems = [];
$scope.isActive = isActive;
$scope.toggleBurgerMenu = toggleBurgerMenu;
$scope.toggleDeviceSettings = toggleDeviceSettings;

$rootScope.$watch('dictionary', function(dictionary) {
if (dictionary) {
init();
}
});

function init() {
$scope.menuConfig = $rootScope.config.navigation;
NavFactory.GetMenuItems().then(onGetMenuItems, $scope.onAPIError);
}

function onGetMenuItems(response) {
$scope.moduleConfig = response.moduleConfig;
$scope.menuItems = response.menuItems;
}
}
})();


This is my Test suite (karma, jasmine)
describe('NavController function', function() {

var $rootScope, $scope, $location, $window, $controller, createController, NavFactory, CacheFactory, toastr;

beforeEach(module('mockedDashboard'));

beforeEach(inject(function(_$rootScope_, _$controller_, _$location_, _$window_, _NavFactory_, _toastr_) {
$location = _$location_;
$window = _$window_;
toastr = _toastr_;

$rootScope = _$rootScope_;
$rootScope.dictionary = jsDictionary;
$rootScope.config = jsConfig;
$rootScope.config.contentFolder = '_default';

$scope = _$rootScope_.$new();
$scope.burgerMenuActive = false;
//mock the parent controller function
$scope.navigate = function(path) {
$location.path('/' + path);
};
// end mock

createController = function() {
return _$controller_('NavController', {
'$scope': $scope
});
};
$controller = createController();
}));

// We are using CacheFactory in this project, when running multiple tests on the controller
// we need to destroy the cache for each test as the controller is initialized for each test.
afterEach(inject(function(_CacheFactory_) {
CacheFactory = _CacheFactory_;
CacheFactory.destroy('defaultCache');
}));

describe('init()', function() {

it('should call NavFactory.GetMenuItems and set moduleConfig and menuItems on $scope', inject(function(_$httpBackend_) {

var $httpBackend = _$httpBackend_;

expect($scope.moduleConfig).toBe(undefined);
expect($scope.menuItems.length).toBe(0);

var responseData = [{
"path": "stats",
"url": "./app/modules/dashboard/modules/score/score.index.html",
"icon": "fa fa-fw fa-pie-chart",
"dashboardEnabled": true,
"burgerMenu": true,
"barMenu": false,
"barMenuOrder": -1,
"bottomMenu": true,
"bottomMenuOrder": 3,
"order_sm": 1,
"order_md": 2,
"order_lg": 2
}];

$httpBackend.expectGET('../content/_default/config/modules.json').respond(responseData);
$httpBackend.flush();

expect($scope.moduleConfig).not.toBe(undefined);
expect($scope.menuItems.length > 0).toBe(true);
}));
});});


I was expecting to have to run a $scope.$digest() in order to fire the watcher, to get init() to run, but it runs fine as is and the tests pass.

This isn't a problem, I'm just trying to understand why it's running because I don't understand how the watcher is firing? what's causing it to run?

Answer

In my opinion, it is a bad idea to initialize the controller in global scope. This is because, in future, when you want to test some other controllers which are logically independent, you would still be initializing each controller for every it() block, for which I see no reason to do so.

As for why your $watch gets fired, you are setting jsDictionary to a $rootScope and initializing it for each it() block. In you controller code, for the $watch you are just checking if the value exists, which it will as you set it in the previous lines and hence it calls the init() block. The correct way of using the $watch would be:

$rootScope.$watch('dictionary', function(oldVal, newVal){
});

A better way of doing would be to create a describe() block for each controller, put your beforeEach() stuff inside the describe() and also your it() block, those that are related to that particular controller. This would also help you to build modular test cases.

Comments