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.

Unit Testing $timeout in AngularJS - ngMock Fundamentals

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

on
on javascript, angularjs, testing, ngmock

This post that covers ngMock’s $timeout service. This acts as a decorator for AngularJS’ $timeout service for the context of unit tests. The $TimeoutDecorator service in ngMock retains the standard methods available in AngularJS’ implementation, with the addition of two new methods that enable us to test code that uses this service:

Method Description
flush([delay]) Flushes/Executes any timeouts that are set in the code under test. We can optionally provide a delay in milliseconds.
verifyNoPendingTasks() Will throw an exception if there are any outstanding timeouts i.e. that were not flushed.

Example Application using $timeout

Consider the following calculator app as an example:

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

app.controller('CalculatorController', function calculatorController($scope, $timeout) {
  $scope.sum = function sum() {
  	$scope.result = $scope.x + $scope.y;
  }

  // add a 3 second delay until we get the answer!
  $scope.sumWithWait = function sumWithWait() {
  	$timeout($scope.sum, 3000);
  }
});

Unit Tesitng $timeout

In the previous posts about angularJS unit testing, we’ve seen many times how we could test the sum function:

it('should set result to 3 for sum', function() {
	var calculatorController = $controller('CalculatorController', { $scope: $scope });
	$scope.x = 1;
	$scope.y = 2;
	$scope.sum();
	expect($scope.result).toBe(3);
});

What if we wanted to test the second function that makes use of angular’s timeout? Do we just call the sumWithWait function as per the following snippet?

it('should set result to 3 for sum', function() {
	var calculatorController = $controller('CalculatorController', { $scope: $scope });
	$scope.x = 1;
	$scope.y = 2;

	// this won't work!
	$scope.sumWithWait();
	
	// $scope.result will be undefined
	expect($scope.result).toBe(3);
});

Unfortunately this won’t work. The test code will be executed synchronously which means that the $scope.result will be undefined when we call the expect function to assert the outcome. What if we used a timeout in the test itself, to wait until the timeout delay has passed before calling expect? Like this:

it('should set result to 3 for sum with timeout (bad example)', function(done) {
  var calculatorController = $controller('CalculatorController', { $scope: $scope });
  $scope.x = 1;
  $scope.y = 2;
  
  $scope.sumWithWait();

  /* We could try with another $timeout */
  $timeout(function assert() {
    expect($scope.result).toBe(3);
    $timeout.verifyNoPendingTasks();
    done();
  }, 1001);

  /* OR */

  /* We could try with JavaScript's timeout (but we shouldn't do this) */
  setTimeout(function() {
    expect($scope.result).toBe(3);
    done();
  }, 1000);

});

Unfortunately, neither of these approaches will work. So, how do we test this asynchronous code in a synchronous fashion? This is where ngMock’s additional functions will help us.

We can update our test, this time injecting an instance of the ngMock $timeout, which will allow us to make use of the two additional functions provided by ngMock’s version. The key addition in this test, is that we call the flush function that’s available via the $timeout api. The flush function causes the timeout in the controller to terminate immediately, thus giving us synchronous execution and ensuring that when we reach the expect function, the sumWithWait function has been executed.

Here’s the test code:

it('should set result to 3 for sum with timeout', inject(function($timeout) {
	var calculatorController = $controller('CalculatorController', { $scope: $scope });
	$scope.x = 1;
	$scope.y = 2;
	$scope.sumWithWait();
	
	// flush timeout(s) for all code under test.
	$timeout.flush();

	// this will throw an exception if there are any pending timeouts.
	$timeout.verifyNoPendingTasks();

	expect($scope.result).toBe(3);
}));

Unit Tesitng $timeout with flush delay

We can optionally supply an argument to the flush function to delay the operation by the given argument (in milliseconds):

$timeout.flush(500); // 0.5 second

When would this be useful? Consider the following trivial function. In this code, we have two calls to the $timeout service. The first does as before, and makes us wait in suspense for 3 seconds before our two numbers are summed. The second will reset the result to zero after another second, or 4 seconds from when the sumWithWaitAndReset function is initially called:

$scope.sumWithWaitAndReset = function sumWithWaitAndReset() {
  // timeout 1: gives us the sum after 3 seconds
  $timeout($scope.sum, 3000);

  // timeout 2: after 4 seconds, the result is set back to zero.
  $timeout(function() { $scope.result = 0; }, 4000);
}

If we call flush as we did before, both timeouts would be executed immediately, and the $scope.result value would always be zero, since after calling sum we would straight away call the next timeout.

In order to test this in sequence, and not to flush both immediately, we would supply an argument to flush (in milliseconds) that would bring us to the desired point in time. This is best explained via the following test code:

it('should set result to 3 for sum with timeout (using delay with flush)', function() {
  var calculatorController = $controller('CalculatorController', { $scope: $scope });
  $scope.x = 1;
  $scope.y = 2;
  $scope.sumWithWaitAndReset();
  
  /* First Timeout */
  // this takes us to the point in time where the first timeout is flushed.
  $timeout.flush(3001);
  expect($scope.result).toBe(3);
  // This should throw an error, because we have not yet flushed the second timeout.
  expect($timeout.verifyNoPendingTasks).toThrow();

  /* Second Timeout */
  // this takes us to 4 seconds since the initial function call, and thus both timeouts have been flushed.
  $timeout.flush(1000); 
  expect($scope.result).toBe(0);
  $timeout.verifyNoPendingTasks();
});

An in Depth Look

In our angular code, we should always use an angular service in place of standard objects that are globally available via the browser, such as window, timeout, interval etc. Angular has $window, $timeout, $interval etc that are wrappers to these objects. We need such wrappers for testability, so that they can be mocked for unit tests.

An observation with our tests using $timeout, is that the code wrapped inside a timeout function never executes even if we created a sufficient delay by via the browser’s timeout. We are required to control the execution of timeouts via the flush function. So how is this achieved?

We already stated that ngMock has a decorator for angular’s $timeout implementation, supplementing the functionality with flush and verifyNoPendingTasks. We can see where the decorator is registered by looking at ngMock’s source code, in particular where the ngMock module is declared:

angular.module('ngMock', ['ng']).provider({
  // other services removed for brevity...
  $browser: angular.mock.$BrowserProvider
  // ...
}).config(['$provide', function($provide) {
  // other services removed for brevity...
  $provide.decorator('$timeout', angular.mock.$TimeoutDecorator);
  // ...
}]);

Note that $timeout has a dependency on $browser, which we will discuss shortly.

The source code for the flush function is very simple and can be found within the $TimeoutDecorator body:

$delegate.flush = function(delay) {
  $browser.defer.flush(delay);
};

The $browser object is ngMock’s fake browser implementation which makes browser functions such as timeout and interval easier to mock. This functionality is in a $BrowserProvider service, and is not featured in the ngMock documentation since it’s a private service (we will take a more in depth look at the $BrowserProvider in a future post). We saw the mock $browser provider being initialised in the module definition. This means that any services from the main angular code (i.e. not just ngMock) will use ngMock’s instance of $browser.

We will see how ngMock’s $timeout and $browser connect with Angular’s $timeout service by examining the following sections of the source code.

Here is the source code of the mock.$browser.defer.flush method that we see is called by the mock.$timeout.flush function we just examined:

self.defer.flush = function(delay) {
  if (angular.isDefined(delay)) {
    self.defer.now += delay;
  } else {
    if (self.deferredFns.length) {
      self.defer.now = self.deferredFns[self.deferredFns.length - 1].time;
    } else {
      throw new Error('No deferred tasks to be flushed');
    }
  }

  while (self.deferredFns.length && self.deferredFns[0].time <= self.defer.now) {
    
    /* This is the line of interest! */
    self.deferredFns.shift().fn();

  }
};

Each time we called $timeout in our controller, a function was added to the array of deferredFns that we see used. This code iterates all of the functions in the array, and if the time is applicable (based on the delay value), they are executed and removed from the array.

If you examine the main angular source code and locate $TimeoutProvider, you will see that the $timeout service itself calls the $browser.defer function when we add a function to the service:


timeoutId = $browser.defer(function() {
  
  /* Body of code removed for brevity */

}, delay);

NB the main angular code would also use mock.$browser in the unit tests.

The $browser.defer function call (from ngMock’s version) simply stores the functions in the same array we see used in the flush function. Here’s the function:

self.defer = function(fn, delay) {
  delay = delay || 0;
  self.deferredFns.push({time:(self.defer.now + delay), fn:fn, id: self.deferredNextId});
  self.deferredFns.sort(function(a, b) { return a.time - b.time;});
  return self.deferredNextId++;
};

The main point is that we understand in angular tests (and the code under test), we are never directly interacting with objects supplied via the browser, and therefore to follow the AngularJS convention, we should always interact via its services, ensuring that our code remains testable.

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