Jasmine Tutorial: Unit Testing Promises in AngularJS Services
So recently I’ve been doing a lot of Angular unit tests with Jasmine and Grunt. I have to say, testing promises is extremely frustrating and most of the examples I found out there either 1) didn’t work or 2) wouldn’t allow you to test content in a then block if you had multiple chained promises, or if you had a promise being called from and returned from another service. After many hours of frustrating trial and errors I found a solution I’m happy with so I want to share.
Let’s assume this is your controller:
'use strict';
angular.module('myModule').controller('myCtrl', ['$scope', 'api',
function ($scope, $api) {
$scope.data = null;
$scope.message = null;
/**
* Load some data from the API service
* @return boolean
**/
$scope.loadData = function () {
var config = {
method: 'GET',
url: 'http://api.domain.com/someRESTfulEndpoint',
headers: {'Content-Type': 'application/json'},
data: null
};
return $api.load(config).then(function (request) {
if (request) {
$scope.message = 'Your data was successfully loaded';
$scope.data = request.response;
return true;
} else {
$scope.message = 'The API service failed';
}
return false;
});
};
}]);
And here is the service making your API calls:
'use strict';
angular.module('myModule').service('api', ['$rootScope', '$http',
function ($rootScope, $http) {
/**
* Makes the $http request given the proper configuration options
* @param Object configruation for the $http request
* @return Object $http promise that resolves to request payload or error
**/
this.load = function (options) {
return $http({
method: options.method || 'GET',
url: options.url || 'http://api.domain.com/someDefaultEndpoint',
headers: options.headers || null,
data: options.data || null
}).then(
function (request) {
//we got a response back, return just the API payload data
if (request.status === 200 && request.data) {
return request.data;
} else {
return {
results: false,
message: 'API request error',
response: request
};
}
}
);
};
}]);
Finally, this is your working unit tests:
'use strict';
describe('testing myModule', function () {
beforeEach(module('myModule'));
var $controller, $rootScope, $scope, $api, $httpBackend, $q;
beforeEach(
inject(function (_$controller_, _$rootScope_, api, _$httpBackend_, _$q_) {
$controller = _$controller_;
$rootScope = _$rootScope_;
$httpBackend = _$httpBackend_;
$q = _$q_;
$api = api;
$scope = $rootScope.$new();
$httpBackend.when('GET').respond('test http GET call');
$httpBackend.when('POST').respond('test http POST call');
$controller('myCtrl', {
$scope: $scope,
$api: $api
});
})
);
it('has a $scope.loadData() function', function() {
var testConfig = {
method: 'POST',
url: 'http://api.domain.com/endpoint',
data: { some: 'data'}
};
var fakeData = { data: 'is fake' };
//we spy on the API service and also return a fake promise call
spyOn($api, 'load').and.callFake(
function() {
var deferred = $q.defer();
deferred.resolve({result: true, response: fakeData});
return deferred.promise;
});
});
$scope.loadData(testConfig); //call the function we're testing in the controller
$scope.$apply(); //we have to call this so Angular will try and resolve the promise
expect($api.load.calls.count()).toEqual(1); //make sure it's called the api service
expect($scope.message).toEqual('Your data was successfully loaded');
expect($scope.data).toEqual(fakeData);
});
it('returns an error if the API fails', function() {
var testConfig = {
method: 'POST',
url: 'http://api.domain.com/endpoint',
data: { some: 'data'}
};
spyOn($api, 'load').and.callFake(
function() {
var deferred = $q.defer();
deferred.resolve({
result: false,
message: 'api error'
});
return deferred.promise;
});
$scope.loadData(testConfig);
$scope.$apply();
expect($scope.message).toEqual('api error');
expect($api.load.calls.count()).toEqual(1);
});
});
