17

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)

Mark Amery
  • 143,130
  • 81
  • 406
  • 459
clement
  • 489
  • 1
  • 5
  • 18

1 Answers1

26

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!

Pascal Precht
  • 8,803
  • 7
  • 41
  • 53
  • 1
    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. – clement Jun 02 '14 at 09:54
  • 1
    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. – Pascal Precht Jun 02 '14 at 09:56
  • 1
    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 ? – clement Jun 02 '14 at 11:00
  • 2
    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) {` – HankCa Feb 07 '15 at 06:01