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