Bradley Braithwaite
1 import {
2 Learning,
3 JavaScript,
4 AngularJS
5 } from 'bradoncode.com'
6 |

How to Unit Test with $rootScope in AngularJS - ngMock Fundamentals

How to unit test AngularJS code that uses the $scope service with ngMock.

on
on javascript, angularjs, testing, ngmock

In this post we take a look at unit testing with ngMock’s $rootScope service. Some knowledge of angular’s $rootScope and child $scope is assumed. ngMock’s version acts as a wrapper for the core angular $rootScope service, giving us access to the standard scope functions we may wish to use in our tests, such as $apply, $digest, $new, $broadcast, $emit etc.

Unit Testing with $rootScope

ngMock’s $rootScope allows us to get an instance of angular’s core $rootScope service in our tests via the injector. We can create child scopes via its $new method, with the most common use case being when creating controllers in unit tests, such as the following example:

beforeEach(inject(function($controller,  $rootScope) {
    // Note the use of the $new() function
    $scope = $rootScope.$new();

    // The new child scope is passed to the controller's constructor argument
	$controller('ExampleController', { $scope: $scope });
}));

Controller with $rootScope and $scope

In cases where a $rootScope and $scope object is used by a controller, as in this example:

app.controller('SearchControllerWithRootScope', function ($rootScope, $scope) {
	$rootScope.globalMenu = [
	  'item 1',
	  'item 2',
	  'item 3'
	];

	$scope.results = [
	  'child item 1',
	  'child item 2',
	  'child item 3'
	];
});

The controller test setup would be as follows. Note how the two constructor arguments are passed to the constructor:

beforeEach(inject(function($controller, _$rootScope_) {
  $rootScope = _$rootScope_;
  $scope = _$rootScope_.$new();

  $controller('SearchControllerWithRootScope', { 
    $rootScope: $rootScope, 
    $scope: $scope
  });
}));

Nested Controllers

The $rootScope object works in the same way as in our angular code. The parent-child relationships of scope are the same, as demonstrated in the following example controller code:

app.controller('NestedController', function ($scope) {
	// note that $scope.query is taken from the parent scope!
	$scope.message = 'You searched for: ' + $scope.query;
});

We could test this controller as follows:

describe('Controller that uses nested Controllers', function () {

	var $controller;
	var $childScope;
	var $scope;

	beforeEach(module('search'));

	beforeEach(inject(function(_$controller_, _$rootScope_) {
	  $controller = _$controller_;
	  $rootScope = _$rootScope_;
	  $scope = _$rootScope_.$new();
	}));

	it('should set rootScope properties', function () {
	  // we create a new child scope object for the controller
	  $childScope = $scope.$new();
	  
	  // we set a property on the parent scope
	  $scope.query = 'angular tutorials';
	  
	  // we initialise the controller, passing our child scope
	  $controller('NestedController', {
	    $scope: $childScope
	  });

	  // we verify in the test, that the controller has used parent and child scope
	  expect($childScope.message).toBe('You searched for: angular tutorials');
	});

});

NB in most cases, this wouldn’t be necessary since testing in isolation, we could just set the property directly on the controller’s scope object. This is a trivial example to illustrate how root and child scope can be setup in tests.

Resolving Promises

It’s a common scenario to unit test code that uses promises via the $q service, as demonstrated in the following controller code:

app.controller('SearchController', function ($scope, searchService) {
	searchService.search($scope.query)
	  .then(function(data) {
	    $scope.results = data;
	  })
	  .catch(function() {
	    $scope.error = 'There has been an error!';
	  });
});

In order to resolve our promises so that we could test this controller, we need to call the scope’s $apply function, as demonstrated in the following test:

describe('Search Tests with Promises', function () {

	var $scope;
	var $rootScope;
	var $q;

	beforeEach(module('search'));

	beforeEach(inject(function($controller, _$rootScope_, _$q_, searchService) {
	  $q = _$q_;
	  $rootScope = _$rootScope_;
	  $scope = _$rootScope_.$new();

	  // Setup the Promise
	  searchServiceDeferred = _$q_.defer();
	  spyOn(searchService, 'search').and.returnValue(searchServiceDeferred.promise);
	  $controller('SearchController', { $scope: $scope, searchService: searchService });
	}));

	it('should resolve promose', function () {
	  // Resolve the promise and call $apply to start digest cycle.
	  searchServiceDeferred.resolve([{ id: 1 }, { id: 2 }]);
	  
	  // The promise won't be resolved until apply is called
	  $rootScope.$apply();

	  expect($scope.results).not.toBe(undefined);
	  expect($scope.error).toBe(undefined);
	});

});

Key Point:

  • We need to call $apply or $digest, as the promises will only resolve as part of angular’s digest cycle, which we can manually trigger via the $rootScope instance.

Other common use-cases for $rootScope are:

  • Calling $rootScope.$digest(); for scenarios where all of the watchers of the current scope and its children need to be processed.

  • When unit testing with the $location service, e.g. we may have controller code such as $location.search('q', 'angularjs testing'); that won’t resolve until the digest loop executes.

Using the Decorator Methods of $rootScope

ngMock adds two utility methods for unit testing. The two additional methods are:

  • $countChildScopes - counts all the direct and indirect child scopes of the current scope.
  • $countWatchers - counts all the watchers of direct and indirect child scopes of the current scope.

These can be especially useful when testing directives.

Use-cases for countChildScopes and countWatchers

Here’s a simple example directive that converts the following html element:

<search-control></search-control>

Into the following html once compiled via angular’s $compile service:

<input type="search" placeholder="{{query}}">

Here’s the code for the directive:

app.directive('searchControl', function() {
	return {
	  replace: true,
	  restrict: 'E',
	  scope: false, // Note that child scope is false
	  template: '<input type="search" placeholder="search...">'
	};
});

To unit test a directive like this, the test code would look as follows:

describe('Directive Test', function () {

	var $compile;
	var $rootScope;

	beforeEach(module('search'));

	beforeEach(inject(function(_$rootScope_, _$compile_) {
	  $rootScope = _$rootScope_;
	  $compile = _$compile_;
	}));

	it('should render directive', function () {

	  // We call the $compile service and pass the $rootScope
	  var element = $compile("<search-control></search-control>")($rootScope);
	  
	  // Unless we trigger the digest cycle, the directive's expressions won't be evaluated.
	  $rootScope.$digest();

	  expect($rootScope.$countChildScopes()).toBe(0);
	  expect($rootScope.$countWatchers()).toBe(0);
	});

});

Key Point:

  • Again we see use of $digest() to trigger the digest cycle.

  • The $countChildScopes() and $countWatchers() both have a value of zero. This is because our directive is set not to have its own scope.

We can inspect the $rootScope object via the angular.mock.dump($rootScope) function. It has functionality to detect a scope object and output debug information so that the scope can be inspected.

Calling console.log(angular.mock.dump($rootScope)); displays the following output, which indicates that we have a single root scope with no properties and no child scope objects:

Scope(1): {

}

If the directive is updated to use scope, as per the following code:

app.directive('searchControlScope', function() {
	return {
	  replace: true,
	  restrict: 'E',
	  scope: true, // sets child scope to true!
	  template: '<input type="search" placeholder="search...">'
	};
});

The values of $countChildScopes() and $countWatchers() will change to be 1 and zero respectively:

expect($rootScope.$countChildScopes()).toBe(1);
expect($rootScope.$countWatchers()).toBe(0);

Using angular.mock.dump($rootScope) once again displays the output:

Scope(1): {
	Scope(2): {
    }
}

This time there is a child scope, since the directive has its own child scope created when the directive is compiled.

The watcher value is zero, because we’re not binding any properties from the scope object in the directive.

If we update the example again, this time to have its own scope and use it in the directive via a binding as follows:

app.directive('searchControlScope', function() {
	return {
	  replace: true,
	  restrict: 'E',
	  scope: true, // sets child scope to true!
	  template: '<input type="search" placeholder="{{placeholder}}">' // uses the binding
	};
});

This is a trivial example, but the directive will now compile the HTML using our given value as the placeholder attribute. For example:

<input type="search" placeholder="OUR CUSTOM TEXT">

We then update the test, setting the placeholder property on the scope object:

it('should render directive', function () {
  
  // this property will be used by the directive
  $rootScope.placeholder = 'search...';
  
  var element = $compile("<search-control-scope></search-control-scope>")($rootScope);
  
  $rootScope.$digest();

  expect($rootScope.$countChildScopes()).toBe(1);
  expect($rootScope.$countWatchers()).toBe(1);
});

Key Point:

  • $countWatchers() now has a value of 1, because we are binding to the $rootScope.placeholder property in the directive.

The output from angular.mock.dump($rootScope) now indicates that we have a property on the root scope object, and there is a child scope:

Scope(1): {
	placeholder: "search..."
  	Scope(2): {

	}
}

The additional methods of ngMock’s $rootScope service used in combination with angular.mock.dump give us a useful insight into the size of our scope hierarchies, in particular when unit testing directives.

Example Test Code

Full code example of the tests used in this post via a Github Gist.

SHARE
Don't miss out on the free technical content:

Subscribe to Updates

CONNECT WITH BRADLEY

Bradley Braithwaite Software Blog Bradley Braithwaite is a software engineer who works for the search engine duckduckgo.com. He is a published author at pluralsight.com. He writes about software development practices, JavaScript, AngularJS and Node.js via his website . Find out more about Brad. Find him via:
You might also like:
mean stack tutorial AngularJS Testing - Unit Testing Tutorials