Bradley Braithwaite
1 import {
2 Learning,
3 JavaScript,
4 AngularJS
5 } from 'Brad on Code.com'
6 |
Check out my online course: AngularJS Unit Testing in-depth with ngMock.

How to Unit Test Exceptions in AngularJS - ngMock Fundamentals

How to unit test Exceptions in AngularJS using ngMock, $exceptionHandler and Jasmine.

on
on javascript, angularjs, testing, ngmock

In this post we look at how we can unit test code that throws exceptions, using AngularJS, ngMock and Jasmine.

There are two main approaches to asserting exceptions:

  • Using Jasmine’s expectations for exceptions
  • Using ngMock’s $exceptionHandler service

Example Application

The following sample app will be used as a working example. The functionality is simple, we can set two number values on the $scope object (x and y) and call a function divide that will perform the division of the two values and set the value of $scope.result to the result. JavaScript is an accommodating language so if we try to divide by zero, the result will be NaN (not a number). In the following code, we would rather throw an exception to guard against ‘divisible by zero’ errors. Here’s the controller code:

var app = angular.module('calculatorApp', []);

app.controller('CalculatorController', function productsController($scope, $timeout) {
  $scope.divide = function divide() {
  	if ($scope.y === 0) {
  		throw 'divisor cannot be zero';
  	}

  	$scope.result = $scope.x / $scope.y;
  }

});

Using Jasmine’s expectations for exceptions

If we wanted to unit test this code and check it throws an exception when the $scope.y value is zero, we could use Jasmine’s built in matchers for asserting exceptions:

describe('exceptions (using Jasmine)', function() {
	beforeEach(inject(function(_$controller_) {
	 	$controller = _$controller_;
	 	$scope = {};
	}));

	it('should return exception when trying to divide by zero (using jasmine)', function() {
		var productsController = $controller('CalculatorController', { $scope: $scope });
		$scope.x = 1;
		$scope.y = 0;
		expect($scope.divide).toThrow('divisor cannot be zero');
	});
});

Key Points:

The toThrow matcher from Jasmine is used to check that the function we call throws an exception. This is the key line:

expect($scope.divide).toThrow('divisor cannot be zero');

Note that we pass the string value for the exception, which must match i.e. if there was a typo in the unit test for the ‘divisor cannot be zero’ argument, the test would fail. Passing the string argument to toThrow is optional, we could just check for any type of exception without passing an argument as per the following:

expect($scope.divide).toThrow();

We could also check that an exception is not thrown via the syntax below:

expect($scope.divide).not.toThrow('divisor cannot be zero');

Using ngMock’s $exceptionHandler service

The previous example using Jasmine works well for synchronous code, but how could we use ngMock’s $exceptionHandler for the same code? Using the service allows us to either rethrow an exception or catch it and store it an array of errors that we can inspect in our tests.

The service is configurable via $exceptionHandlerProvider so that we can set whether to rethrow or log exceptions in the code under test. We can do this using the syntax as follows:

beforeEach(module(function($exceptionHandlerProvider) {
	$exceptionHandlerProvider.mode('log');
	/* OR */
	$exceptionHandlerProvider.mode('rethrow');
}));

Rethrow does as the name suggests and re-throws an exception. Log catches the exception(s) storing each one in an array, so that we can verify the contents of the errors array in our tests.

We can inject an instance of the $exceptionHandler in the usual fashion, so that we can use the instance in our tests. Here’s example code:

beforeEach(inject(function(_$exceptionHandler_) {
	$exceptionHandler = _$exceptionHandler_;
}));

For synchronous code like our divide function, whether we have configured $exceptionHandlerProvider with log or rethrow configuration, the test code is the same as we presented using Jasmine:

expect($scope.divide).toThrow('divisor cannot be zero');

NB the presence of $exceptionHandler doesn’t make any difference in this instance.

To make things a little more interesting, we will add a new asynchronous method to our controller. This new function wraps the logic we wish to test (the divide function) in the Angular $timeout service:

$scope.divideAndWait = function divideAndWait() {
  $timeout($scope.divide, 3000);
}

So, how would we go about testing this?

Testing code using rethrow

We would first need to set the mode to be rethrow:

beforeEach(module(function($exceptionHandlerProvider) {
	$exceptionHandlerProvider.mode('rethrow');
}));

NB rethrow is the default, so setting the mode is optional.

Using the rethrow mode we can assert the error in the same way:

it('should return exception when trying to divide by zero (using timeout)', function() {
	var productsController = $controller('CalculatorController', { $scope: $scope });
	$scope.x = 1;
	$scope.y = 0;
	$scope.divideAndWait();
	expect($timeout.flush).toThrow('divisor cannot be zero');
});

Since rethrow never catches the exception, we would always need to wrap the function that raises the error in Jasmine’s expect function and use toThrow.

Testing code using log

To test the divide and divideAndWait functions using log mode, we would first need to set the mode via the provider:

beforeEach(module(function($exceptionHandlerProvider) {
  $exceptionHandlerProvider.mode('log');
}));

For the divide function, it would be logical to write a test as follows:

it('should return exception when trying to divide by zero (incorrect example)', function() {
	var productsController = $controller('CalculatorController', { $scope: $scope });
	$scope.x = 1;
	$scope.y = 0;

	expect($exceptionHandler.errors).toEqual([]);

	$scope.divide();

	expect($exceptionHandler.errors).toEqual(['divisor cannot be zero']);
});

However, this will not work and the test fails at the line $scope.divide();, since there is nothing to catch the exception.

We would need to catch the exception manually, for example:

it('should return exception when trying to divide by zero (using try catch)', function() {
	var productsController = $controller('CalculatorController', { $scope: $scope });
	$scope.x = 1;
	$scope.y = 0;

	expect($exceptionHandler.errors).toEqual([]);

	try {
		$scope.divide();
	} catch(e) {
		$exceptionHandler(e);
	}

	expect($exceptionHandler.errors).toEqual(['divisor cannot be zero']);
});

Key Points:

When using the exception handler, we can check the errors in our tests using syntax such as:

expect($exceptionHandler.errors).toEqual(['divisor cannot be zero']);

Note that we are using a try/catch in the test body to catch the exception. This is a little ugly to read, and is therefore not a recommended approach, but it’s a good example to demonstrate how the $exceptionHandler service works!

In the catch block we are calling $exceptionHandler(e);, which is where the error is intercepted and logged to the error array. Since log mode does not rethrow the error, the execution of our test can continue.

Next, if we examine the following test, it will be clearer to understand how the errors are logged:

it('should return exception when trying to divide by zero (using timeout)', function() {
	var productsController = $controller('CalculatorController', { $scope: $scope });
	$scope.x = 1;
	$scope.y = 0;

	$scope.divideAndWait();

	expect($exceptionHandler.errors).toEqual([]);

	// execute the timeout
	$timeout.flush();

	expect($exceptionHandler.errors).toEqual(['divisor cannot be zero']);
});

Key Points:

We do not need the try/catch block in this test, since the divideAndWait function uses the $timeout service. In the angular source code, some services such as $timeout will call $exceptionHandler(e); as a convention when an exception is caught. This means that execution is not stopped and in the event of multiple errors, they will all be logged by the error handler service.

Deeper Dive

ngMock has its own implementation that replaces angular’s $exceptionHandlerProvider when it’s is initialised. You can find where it is registered when the ngMock module is defined (taken from source code of ngMock):

angular.module('ngMock', ['ng']).provider({
  
 // other services removed for brevity...
 $exceptionHandler: angular.mock.$ExceptionHandlerProvider
  
});

In the main implementation, the exception is logged via the $log service’s error logging level e.g. $log.error(‘something went wrong’);. If you would like to know more about $log, see the post Unit Testing $log in AngularJS.

ngMock’s version of $ExceptionHandlerProvider doesn’t use $log, it stores any caught exceptions (when using log mode) in an array (var errors = []; if you consult the source code). This array is exposed via the service instance object named .errors. You can see the source code for the function: angular.mock.$ExceptionHandlerProvider.

The following test succinctly demonstrates the main features of using $exceptionHandler with log mode:

describe('inline example', function() {
	beforeEach(module(function($exceptionHandlerProvider) {
	$exceptionHandlerProvider.mode('log');
}));

it('should demonstrate exception handling using log mode', inject(function ($log, $exceptionHandler, $timeout) {
		
	$timeout(function() { $log.log('hello'); });
	$timeout(function() { throw 'goodbye'; });
	$timeout(function() { $log.log('world'); });
	$timeout(function() { throw 'cruel'; });
	$timeout(function() { throw 'world'; });

	// we can pass comma separated values
	$timeout(function() { $exceptionHandler(1,2,3); });

	$timeout.flush();

	// code still executes even after an exception
	expect($log.log.logs[0]).toEqual(['hello']);
	expect($log.log.logs[1]).toEqual(['world']);

	// mutiple errors can be logged, rather than terminiating after first exception
	expect($exceptionHandler.errors[0]).toEqual('goodbye');
	expect($exceptionHandler.errors[1]).toEqual('cruel');
	expect($exceptionHandler.errors[2]).toEqual('world');

	// comma separated args passed to $exceptionHandler are saved as an array
	expect($exceptionHandler.errors[3]).toEqual([1,2,3]);

});

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 search engine start-ups. 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