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

Unit Testing with Promises in AngularJS made Easier

A Tip for Unit Testing with $q Promises in AngularJS with ngMock.

on
on javascript, angularjs, testing, ngmock

This post demonstrates a utility function that I use in my AngularJS unit tests to make the test code for promises more readable, and to reduce some boiler plate code. For a primer on unit testing with promises, see this post Unit Testing with $q Promises in AngularJS.

To setup some context, consider what a unit test would look like for this controller code:

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!';
	  });
});

We might write a test like along the lines of this:

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!');
  });

});

In this test there are a few areas of complexity and duplication that could be improved upon. Such as:

  • We have to manually include the $q angular library and create a new deferred instance via _$q_.defer();.

  • We then need to use Jasmine’s spyOn function in order to return a promise via our new deferred instance for the service function called in the controller code.

  • In the test, we need to setup the resolve or reject function e.g. deferred.resolve([{ id: 1 }, { id: 2 }]); that links back to the spyOn function’s returned promise.

  • We are also required to call $scope.$apply(); in order for our calls to deferred.resolve or deferred.reject to be executed, thus connecting everything together.

This solution aims to remove these complexities from the test code, resulting in the following simplified code for the same test:

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

  beforeEach(module('testUtils'));
  beforeEach(module('search'));

  beforeEach(inject(function($controller, _$rootScope_, searchService, _utils_) {
    $scope = _$rootScope_.$new();
    utils = _utils_;
    
    $controller('SearchController', { 
      $scope: $scope, 
      searchService: searchService
    });
  }));

  it('should resolve promise', function () {
    utils.resolvePromise(searchService, 'search', [{ id: 1 }, { id: 2 }]);
    $scope.$apply();
    expect($scope.results).not.toBe(undefined);
    expect($scope.error).toBe(undefined);
  });
  
  it('should reject promise', function () {
    utils.rejectPromise(searchService, 'search');
	$scope.$apply();
    expect($scope.results).toBe(undefined);
    expect($scope.error).toBe('There has been an error!');
  });

});

Now all that’s required is to:

  • Include the module for use in the test: beforeEach(module('testUtils'));

  • To resolve or reject the promise via a single line of code:

    • To resolve the promise: utils.resolvePromise(myService, ‘theFunctionName’, myDataToReturn);

    • To reject the promise: utils.rejectPromise(myService, ‘theFunctionName’);

Here’s the code for the testUtils module:

'use strict';

angular.module('testUtils', [])
  .factory('utils', function utilsFactory($q) {

  var setupPromise = function(object, method, data, resolve) {
    spyOn(object, method).and.callFake(function() {
        var deferred = $q.defer();
        if (resolve) {
          deferred.resolve(data);
        } else {
          deferred.reject(data);
        }

        return deferred.promise;
      });
  };

  var service = {};

  service.resolvePromise = function(object, method, data) {
    return setupPromise(object, method, data, true);
  };

  service.rejectPromise = function(object, method, data) {
    return setupPromise(object, method, data, false);
  };

  return service;
});
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