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 with $q Promises in AngularJS

How to Unit Test AngularJS components that use $q Promises

on
on angularjs, testing, recipies

This post will teach you how to write unit tests for your AngularJS components that use promises. The most common type of test I have to write that uses promises is for controllers that have services injected with asynchronous functions.

Controller that uses Promises

Here’s an example controller that uses a service with an asynchronous function:

app.controller('SearchController', function ($scope, searchService) {
	// The search service returns a promise API
	searchService.search($scope.query)
	  .then(function(data) {
      // This is set when the promise is resolved.
	    $scope.results = data;
	  })
	  .catch(function() {
      // This is set in the event of an error.
	    $scope.error = 'There has been an error!';
	  });
});

Key Points:

  • The call the searchService.search returns a promise, because it’s an asynchronous operation.

  • The promise instance returned wraps the asynchronous logic from the search function with a .then function (called when the operation is completed successfully) and a .catch function (called when something went wrong and it didn’t work).

To provide the full example, here’s the code for searchService that’s used by the controller:

app.factory('searchService', function ($q, $http) {
	var service = {};

	service.search = function search (query) {
    // We make use of Angular's $q library to create the deferred instance
		var deferred = $q.defer();

  	$http.get('http://localhost/v1?=q' + query)
    		.success(function(data) {
          // The promise is resolved once the HTTP call is successful.
      		deferred.resolve(data);
    		})
    		.error(function() {
          // The promise is rejected if there is an error with the HTTP call.
      		deferred.reject();
    		});

    // The promise is returned to the caller
  	return deferred.promise;
	};

	return service;
});

Key Points:

  • We use Angular’s $q service to create the promise.

  • What’s a Promise anyway? In short, it’s a pattern for making asynchronous code easier to work with i.e. an alternative to passing around lots of callback functions. For more reading, check out MDN’s article: Promise.

  • The resolve function links to the .then function in our controller i.e. all is well, so we can keep our promise and resolve it.

  • The reject function links to the .catch function in our controller i.e. something went wrong, so we can’t keep our promise and need to reject it.

Unit Test for Controller that uses Promises

If you’re in a hurry for the solution, here is a unit test for the controller that mocks the service call. It includes an example for resolving and rejecting the promise. Read on for a more detailed breakdown of what the code is doing.

Here’s the code:

describe('Testing a Controller that uses a Promise', function () {
  var $scope;
  var $q;
  var deferred;

  beforeEach(module('search'));

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

    // We use the $q service to create a mock instance of defer
    deferred = _$q_.defer();
    
    // Use a Jasmine Spy to return the deferred promise
    spyOn(searchService, 'search').and.returnValue(deferred.promise);
    
    // Init the controller, passing our spy service instance
    $controller('SearchController', { 
      $scope: $scope, 
      searchService: searchService
    });
  }));

  it('should resolve promise', function () {
    // Setup the data we wish to return for the .then function in the controller
    deferred.resolve([{ id: 1 }, { id: 2 }]);
    
    // We have to call apply for this to work
    $scope.$apply();

    // Since we called apply, not we can perform our assertions
    expect($scope.results).not.toBe(undefined);
    expect($scope.error).toBe(undefined);
  });
  
  it('should reject promise', function () {
    // This will call the .catch function in the controller
    deferred.reject();
    
    // We have to call apply for this to work
    $scope.$apply();

    // Since we called apply, not we can perform our assertions
    expect($scope.results).toBe(undefined);
    expect($scope.error).toBe('There has been an error!');
  });

});

Key Points:

  • The most common sticking point with testing promises with the $q service is that we need to call $scope.$apply(); before we can assert values in the test.

  • To test in isolation, we make use of Jasmine’s spyOn function to return a mock promise instance when the search function is called in the controller logic that is being tested.

In Depth Explanation

If the code we just looked at feels a little too magical, read on and let’s break down the different elements used in the test.

For a more succinct example, in this test we are going to test with promises directly and not include any additional code (controller, service etc). Here is the code in full, we will break down each concept after the code:

describe('Testing $q directly', function () {
  var deferred;
  var $q;
  var $rootScope;

  beforeEach(inject(function(_$q_, _$rootScope_) {
    $q = _$q_;
    $rootScope = _$rootScope_;
    deferred = _$q_.defer();
  }));

  it('should resolve promise', function () { 
    var response;

    deferred.promise.then(function(data) {
      response = data;
    });

    deferred.resolve('Returned OK!');

    $rootScope.$apply();

    expect(response).toBe('Returned OK!');
  });
  
  it('should reject promise', function () {
    var response;

    deferred.promise.catch(function(data) {
      response = data;
    });

    deferred.reject('There has been an Error!');

    $rootScope.$apply();

    expect(response).toBe('There has been an Error!');
  });

});

Now let’s work through the code from top to bottom. Starting with the beforeEach block:

beforeEach(inject(function(_$q_, _$rootScope_) {
  $q = _$q_;
  $rootScope = _$rootScope_;
  deferred = _$q_.defer();
}));

Key Points:

  • This code is called before each unit test to inject the core angular services we need.

  • We use the $q instance to create a new deferred object that we can use for the tests. We do this by assigning the result of _$q_.defer(); to the local variable named deferred. A deferred object represents a function that is asynchronous.

  • We need the $rootScope instance so that we can call $rootScope.$apply() (we will explain why shortly). See Unit Test with $rootScope for more detail on using $rootScope.

Next we will take a look at the first test:

it('should resolve promise', function () { 
  var response;

  deferred.promise.then(function(data) {
    response = data;
  });

  deferred.resolve('Returned OK!');

  $rootScope.$apply();

  expect(response).toBe('Returned OK!');
});

Key Points:

  • We use the instance of the deferred object we created in the beforeEach block to access the promise instance. Note that the service example earlier in the post returned deferred.promise, here we are using that object directly to simplify the example.

  • From the deferred.promise object we can access the then function that will be called if/when the promise is resolved. The then function sets the data argument to a local variable (called response) that we can use in the test assertion.

  • Calling deferred.resolve('Returned OK!'); resolves our deferred.promise instance, which in turn will invoke the then function passing along the value given in the call to resolve i.e. the string value 'Returned OK!' will be the value of the data argument in the callback function passed to our then function.

  • We must call $rootScope.$apply(); for this to work. If we don’t, the value of the response variable will be undefined. Why is this necessary? Because any promises made with the $q service to be resolved/rejected are processed upon each run of angular’s digest cycle. Conceptually, the call to .resolve changes the state of the promise and adds it to a queue. Each time angular’s digest cycle runs, any outstanding promises will be processed and removed from the queue. For a little more detail on the digest cycle, see the diagram at the end of What are Scopes? from the official angular docs.

  • Finally, we can check that everything worked as expected by checking that the response variable has the same string argument we passed to the resolve function.

Next we will take a look at the second and final test:

it('should reject promise', function () {
  var response;

  deferred.promise.then(function(data) {
    response = data;
  });

  deferred.promise.catch(function(data) {
    response = 'Error: ' + data;
  });

  deferred.reject('500 Status');
  $rootScope.$apply();

  expect(response).toBe('Error: 500 Status');
});

Key Points:

  • The principles of this test are the same as with the resolved promise test we just discussed.

  • In this test, we call deferred.reject thus causing the promise to fail. In turn this means that .catch is called and .then will not be called.

  • In this test we verify that the response value is the error message that we expect.

Putting it all Together

If we revisit the first unit test presented in the post, we can see how we are using Jasmine to create a mock for the search function and to return a deferred promise instance:

spyOn(searchService, 'search').and.returnValue(deferred.promise);

In the controller code, it should now be clearer how the deferred.promise instance we created via the Jasmine mock is being used to generate the .then and .catch functions:

searchService.search($scope.query)
    .then(function(data) {
      // This is set when the promise is resolved.
      $scope.results = data;
    })
    .catch(function() {
      // This is set in the event of an error.
      $scope.error = 'There has been an error!';
    });

This is a slightly longer form of the deferred.promise.then and deferred.promise.catch functions we explored in the more succinct example.

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