Mocking Angular’s $http Promise return type


The Angular $http service provides an easy way to interact with an API endpoint. If you want to query some data, you can simply do this:

var request = $http.get('/api/items');
request.success(function (data) {
  console.log('I have data', data);
});

The request object above is an Angular HTTP promise (ng.IHttpPromise if you’re using TypeScript).

While easy enough to use, it can become a little problematic when unit testing.

Promise Service

Say we have a service that returns a promise:

var ContactsService = (function () {
  function ContactsService($http) {
    this.$http = $http;
  }
 
  ContactsService.prototype.getContacts = function () {
    return this.$http.get('/api/contacts');
  };
 
  return ContactsService;
})();

And then we have a controller that uses this service:

var ContactsController = (function () {
  function ContactsController(ContactsService) {
    var _this = this;
 
    var request = ContactsService.getContacts();
    request.success(function (contacts) {
      _this.contacts = contacts;
    });
 
    request.error(function () {
      throw new Error('Error getting contacts');
    });
  }
 
  return ContactsController;
})();

When you want to test this (as you should), it’s a little tricky to mock the promise return type of the service.

So we’ll build a helper to do the main bits.

Building a Promise

For our helper class definition, we want the following public methods:

  • constructor: create a new helper
  • getHttpPromiseMock: get a mock promise return type
  • resolve: trigger promise fulfilment with data
  • reject: trigger promise rejection

To build up our own mock promise, we’ll use Angular’s $q service. This will let us define our return data and when to fulfil/reject the promise.

For triggering fulfilment, we’ll also need access to $rootScope.$apply() as this is integrated with $q.

constructor

Our constructor will use Angular’s $injector to get access to $q and $rootScope. We want an instance of the deferred object that we can work with within the class, so we create that here.

function PromiseHelper() {
  var _this = this;
  inject(function ($injector) {
    var $q = $injector.get('$q');
    _this._deferred = $q.defer();
    _this.$rootScope = $injector.get('$rootScope');
  });
}

getHttpPromiseMock

For our mock object, we want to create something that looks and acts like a promise.

PromiseHelper.prototype.getHttpPromiseMock = function () {
  var promise = this._deferred.promise;
  return wrapPromise(promise);
};

The success and error methods on our mock promise need to return the same mock instances, so we can pull out a method to wrap up the deferred promise as needed.

function wrapPromise(promise) {
  return {
    then: promise.then,
    success: function (fn) {
      promise.then(fn);
      return wrapPromise(promise);
    },
    error: function (fn) {
      promise.then(null, fn);
      return wrapPromise(promise);
    },
  };
}

This is pretty much following the structure of the Angular wrapper.

We’re passing in a the promise property of a defer object.

We then create the return object that looks like the HTTP promise. Calls to then are just passed through directly to the created promise object from $q.

For success and error, we use different calls to $q‘s then method to simulate the required functionality, finally returning the wrapped promise.

resolve

To resolve the promise, we just need to pass in some data for the deferred‘s resolve function then call $apply on the $rootScope to propagate it.

PromiseHelper.prototype.resolve = function (data) {
  this._deferred.resolve(data);
  this.$rootScope.$apply();
};

reject

To reject the promise, we can call reject on the deferred and then $apply on the $rootScope.

PromiseHelper.prototype.reject = function () {
  this._deferred.reject();
  this.$rootScope.$apply();
};

Our full helper class looks like this:

var PromiseHelper = (function () {
  function wrapPromise(promise) {
    return {
      then: promise.then,
      success: function (fn) {
        promise.then(fn);
        return wrapPromise(promise);
      },
      error: function (fn) {
        promise.then(null, fn);
        return wrapPromise(promise);
      },
    };
  }
 
  function PromiseHelper() {
    var _this = this;
    inject(function ($injector) {
      var $q = $injector.get('$q');
      _this._deferred = $q.defer();
      _this.$rootScope = $injector.get('$rootScope');
    });
  }
 
  PromiseHelper.prototype.resolve = function (data) {
    this._deferred.resolve(data);
    this.$rootScope.$apply();
  };
 
  PromiseHelper.prototype.reject = function () {
    this._deferred.reject();
    this.$rootScope.$apply();
  };
 
  PromiseHelper.prototype.getHttpPromiseMock = function () {
    var promise = this._deferred.promise;
    return wrapPromise(promise);
  };
 
  return PromiseHelper;
})();

So, now that we have that, how do we use it?

Testing Methods that Return a Promise

Just as a reminder, our controller looks like this:

var ContactsController = (function () {
  function ContactsController(ContactsService) {
    var _this = this;
 
    var request = ContactsService.getContacts();
    request.success(function (contacts) {
      _this.contacts = contacts;
    });
 
    request.error(function () {
      throw new Error('Error getting contacts');
    });
  }
 
  return ContactsController;
})();

We’ll need some test data along with our helper, so we’ll set these up in our beforeEach function of our Jasmine test. We’ll also create a new instance of the PromiseHelper.

var contacts,
    promiseHelper;
 
beforeEach(function () {
  // Test data
  contacts = [];
  for (var i = 0; i < 100; i++) {
    contacts.push({
      id: i,
      name: "Contact " + i
    });
  }
 
  // Promise helper
  promiseHelper = new PromiseHelper();
});

Then, in our tests, we can use Jasmine mocking to create a version of our service.

var contactsServiceMock = jasmine.createSpyObj('ContactsService', [
  'getContacts',
]);
 
contactsServiceMock.getContacts.and.returnValue(
  promiseHelper.getHttpPromiseMock(),
);

Just for clarity, this is firstly creating a new Jasmine Spy object with the function getContacts.

Then we’re setting the return type of this Spy to return a mock HTTP promise from our helper class.

Putting this together with a full test then looks something like this:

it('Contact list matches companies from the data store', function () {
  // Arrange
  var contactsServiceMock = jasmine.createSpyObj('ContactsService', [
    'getContacts',
  ]);
 
  contactsServiceMock.getContacts.and.returnValue(
    promiseHelper.getHttpPromiseMock(),
  );
 
  var ctrl = new ContactsController(contactsServiceMock);
 
  // Act
  promiseHelper.resolve(contacts);
 
  // Assert
  expect(ctrl.contacts).toBeDefined();
  expect(ctrl.contacts).toBe(contacts);
 
  expect(contactsServiceMock.getContacts).toHaveBeenCalled();
});

On line 12, we call the resolve method to propagrate our contacts data then we can check the validity of the internal properties. Since the methods within the mock service are Jasmine Spies, we can explicitly check that the method has been called on line 18.

Testing for Rejection

If we wanted to test for a rejected promise, our test would look like this:

it('Throws error when getContacts fails.', function () {
  // Arrange
  var contactsServiceMock = jasmine.createSpyObj('ContactsService', [
    'getContacts',
  ]);
 
  contactsServiceMock.getContacts.and.returnValue(
    promiseHelper.getHttpPromiseMock(),
  );
 
  var ctrl = new ContactsController(contactsServiceMock);
 
  // Act
  expect(function () {
    promiseHelper.reject();
  }).toThrow();
 
  // Assert
  expect(ctrl.contacts).not.toBeDefined();
 
  expect(contactsServiceMock.getContacts).toHaveBeenCalled();
});

Our controller throws an error when the service rejects the promise, so we can check for this using Jasmine’s toThrow method on the promise call (lines 12-14).

Sample Code

Sample code for this is available on GitHub.