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.