Mocking Angular’s $http Promise return type using only Mocks


Last week, we had a look at Mocking Angular’s $http Promise return type using the $q library. This gives you a great deal of control over when your mock promises are resolved. But, it comes at the cost of requiring both the $q library and manually calling $rootscope.$apply().

If you want to avoid those (and you don’t mind losing that bit of control), then you can do something very similar just with mocks.

Promise Service

We’ll use the same service that returns a promise as before:

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

And the same controller using the 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;
})();

Building a Promise

Our mock-only promise helper will have slightly different public methods from our previous version. When using mocks, we’ll set up our methods to immediately respond to requests, so we need to set up return handlers at the start of the test.

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

We’ll build up our mock object using Jasmine’s Spies.

constructor

Our constructor uses Jasmine to create a new mock object with the public methods we need to match the HTTP promise return type.

function PromiseHelper() {
  this._promise = jasmine.createSpyObj('promise', [
    'success',
    'error',
    'finally',
    'then',
  ]);
 
  this._promise.error.and.returnValue(this._promise);
  this._promise.success.and.returnValue(this._promise);
  this._promise.finally.and.returnValue(this._promise);
  this._promise.then.and.returnValue(this._promise);
}

Since the return object is chainable, we set default return value on each of the methods to return the same promise. This means that the mock will fulfil the minimal implementation on creation.

getHttpPromiseMock

For our mock object, we want to create something that looks and acts like a promise. We already have the mock object created in the class, so we just need to return a reference to that.

PromiseHelper.prototype.getHttpPromiseMock = function () {
  return this._promise;
};

willResolveWith

For promise resolution, the promise will accept a callback to the success method. This callback will be called with the return data from the request. We need to mock this.

We can pass in the fulfilment data to the willResolveWith method of our mock and use Jasmine’s callFake function to set up a dummy method that can be called. This dummy method will invoke the callback with the test data and then return an instance of the mock promise for chaining.

PromiseHelper.prototype.willResolveWith = function (data) {
  var _this = this;
  this._promise.success.and.callFake(function (callback) {
    callback(data);
    return _this._promise;
  });
};

willReject

Our willReject method is very similar. We can use the same callFake function to set up a dummy handler to handle error calls on the mock and pass through the promise for chaining.

PromiseHelper.prototype.willReject = function () {
  var _this = this;
  this._promise.error.and.callFake(function (callback) {
    callback();
    return _this._promise;
  });
};

Our full helper class looks like this:

var PromiseHelper = (function () {
  function PromiseHelper() {
    this._promise = jasmine.createSpyObj('promise', [
      'success',
      'error',
      'finally',
      'then',
    ]);
 
    this._promise.error.and.returnValue(this._promise);
    this._promise.success.and.returnValue(this._promise);
    this._promise.finally.and.returnValue(this._promise);
    this._promise.then.and.returnValue(this._promise);
  }
 
  PromiseHelper.prototype.willResolveWith = function (data) {
    var _this = this;
    this._promise.success.and.callFake(function (callback) {
      callback(data);
      return _this._promise;
    });
  };
 
  PromiseHelper.prototype.willReject = function () {
    var _this = this;
    this._promise.error.and.callFake(function (callback) {
      callback();
      return _this._promise;
    });
  };
 
  PromiseHelper.prototype.getHttpPromiseMock = function () {
    return this._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(),
  );
 
  promiseHelper.willResolveWith(contacts);
 
  // Act
  var ctrl = new ContactsController(contactsServiceMock);
 
  // Assert
  expect(ctrl.contacts).toBeDefined();
  expect(ctrl.contacts).toBe(contacts);
 
  expect(contactsServiceMock.getContacts).toHaveBeenCalled();
});

On lines 7-9, we’re setting up the return data for our promise when success is called. Since the methods within the mock service are Jasmine Spies, we can explicitly check that the method has been called on line 20.

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(),
  );
 
  promiseHelper.willReject();
 
  // Act
  var ctrl;
  expect(function () {
    ctrl = new ContactsController(contactsServiceMock);
  }).toThrow();
 
  // Assert
  expect(ctrl).not.toBeDefined();
 
  expect(contactsServiceMock.getContacts).toHaveBeenCalled();
});

On lines 7-9, we’re setting up our promise to reject.

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 15-17).

Sample Code

Sample code for this is available on GitHub.