mock angular service/promise in a karma/jasmine test

23,993

So you want to test, if your service responses as expected? Then, this is something you would rather test on the service. Unit test promise based methods could look like this:

var mapService, $httpBackend, $q, $rootScope;

beforeEach(inject(function (_mapService_, _$httpBackend_, _$q_, _$rootScope_) {
  mapService = mapService;
  $httpBackend = _$httpBackend_;
  $q = _$q_;
  $rootScope = _$rootScope_;

  // expect the actual request
  $httpBackend.expect('GET', '/onmap/rest/map/uuid?editor=true');

  // react on that request
  $httpBackend.whenGET('/onmap/rest/map/uuid?editor=true').respond({
    success: {
      elements: [1, 2, 3]
    }
  });
}));

As you can see, you don't need to use $injector, since you can inject your needed services directly. If you wanna use the correct service names throughout your tests, you can inject them with prefixed and suffixed "_", inject() is smart enough to recognise which service you mean. We also setup the $httpBackend mock for each it() spec. And we set up $q and $rootScope for later processing.

Here's how you could test that your service method returns a promise:

it('should return a promise', function () {
  expect(mapService.getMapUuid('uuid', true).then).toBeDefined();
});

Since a promise always has a .then() method, we can check for this property to see if it's a promise or not (of course, other objects could have this method too).

Next you can test of the promise you get resolves with the proper value. You can do that setting up a deferred that you explicitly resolve.

it('should resolve with [something]', function () {
  var data;

  // set up a deferred
  var deferred = $q.defer();
  // get promise reference
  var promise = deferred.promise;

  // set up promise resolve callback
  promise.then(function (response) {
    data = response.success;
  });

  mapService.getMapUuid('uuid', true).then(function(response) {
    // resolve our deferred with the response when it returns
    deferred.resolve(response);
  });

  // force `$digest` to resolve/reject deferreds
  $rootScope.$digest();

  // make your actual test
  expect(data).toEqual([something]);
});

Hope this helps!

Share:
23,993
clement
Author by

clement

Updated on April 26, 2020

Comments

  • clement
    clement about 4 years

    I'm trying to write a karma/jasmine test and I would like some explanations about how mocks are working on a service which is returning a promise. I explain my situation :

    I have a controller in which I do the following call :

    mapService.getMapByUuid(mapUUID, isEditor).then(function(datas){
        fillMapDatas(datas);
    });
    
    function fillMapDatas(datas){
        if($scope.elements === undefined){
            $scope.elements = [];
        }
     //Here while debugging my unit test, 'datas' contain the promise javascript object instead //of my real reponse.
       debugger;
        var allOfThem = _.union($scope.elements, datas.elements);
    
        ...
    

    Here is how my service is :

    (function () {
    'use strict';
    
    var serviceId = 'mapService';
    
    angular.module('onmap.map-module.services').factory(serviceId, [
        '$resource',
        'appContext',
        'restHello',
        'restMap',
        serviceFunc]);
    
    function serviceFunc($resource, appContext, restHello, restMap) {
    
        var Maps = $resource(appContext+restMap, {uuid: '@uuid', editor: '@editor'});
    
        return{          
            getMapByUuid: function (uuid, modeEditor) {
                var maps = Maps.get({'uuid' : uuid, 'editor': modeEditor});
                return maps.$promise;
            }
        };
    }
    
    })();
    

    And finally, here is my unit test :

    describe('Map controller', function() {
    var $scope, $rootScope, $httpBackend, $timeout, createController, MapService, $resource;
    
    beforeEach(module('onmapApp'));
    
    beforeEach(inject(function($injector) {
        $httpBackend = $injector.get('$httpBackend');
        $rootScope = $injector.get('$rootScope');
        $scope = $rootScope.$new();
    
        var $controller = $injector.get('$controller');
    
        createController = function() {
            return $controller('maps.ctrl', {
                '$scope': $scope
            });
        };
    }));
    
    afterEach(function() {
        $httpBackend.verifyNoOutstandingExpectation();
        $httpBackend.verifyNoOutstandingRequest();
    });
    
    var response = {"elements":[1,2,3]};
    
    it('should allow user to get a map', function() {
    
        var controller = createController();
        $httpBackend.expect('GET', '/onmap/rest/map/MY-UUID?editor=true')
            .respond({
                "success": response
            });
    
    
    // hope to call /onmap/rest/map/MY-UUID?editor=true url and hope to have response as the fillMapDatas parameter
        $scope.getMapByUUID('MY-UUID', true); 
    
        $httpBackend.flush();
    });
    });
    

    What I really want to do is to have my response object ( {"elements:...}) as the datas parameter of the fillMapDatas function. I don't understand how to mock all the service things (service, promise, then)

  • clement
    clement almost 10 years
    Thanks, that seems very good. Just one question (I'm a very beginer with angular) : is it a good way to return the promise in my service and use "then" function in my controller as I did please ? I'm not very sure about that.
  • Pascal Precht
    Pascal Precht almost 10 years
    Well, since it's an asynchronous task, it's totally fine to use promise for your service method. This is how you actually should do things that are asynchronous.
  • clement
    clement almost 10 years
    Sorry but actually I don't understand the last "it" function. On expect(data).toEqual([something]); "data" is always undefined. If i put a "debugger" instruction in promise.then function, unit test is never goes on it. Do you have any idea please ?
  • HankCa
    HankCa over 9 years
    Great work, thanks for that. It requires a few corrections to be complete. 1. Your example is missing $httpBackend.flush(); (it is in the question); 2. mapService.getMapUuid('uuid', true).then(response) { should read mapService.getMapUuid('uuid', true).then(function(response) {