0

I want to write the unit test for the factory which have lot chain of promises. Below is my code snippet:

angular.module('myServices',[])
.factory( "myService",
    ['$q','someOtherService1', 'someOtherService2', 'someOtherService3',  'someOtherService4',
    function($q, someOtherService1, someOtherService2,  someOtherService3,  someOtherService4) {

method1{
 method2().then(
  function(){ someOtherService3.method3();},
  function(error){/*log error;*/}
 );
 return true;
};

var method2 = function(){
 var defer = $q.defer();
 var chainPromise = null;
 angular.forEach(myObject,function(value, key){
  if(chainPromise){
   chainPromise = chainPromise.then(
    function(){return method4(key, value.data);},
    function(error){/*log error*/});
  }else{
   chainPromise = method4(key, value.data);
  }
 });

 chainPromise.then(
  function(){defer.resolve();},
  function(error){defer.reject(error);}
 );
 return defer.promise;
};

function method4(arg1, arg2){
 var defer = $q.defer();
 someOtherService4.method5(
  function(data) {defer.resolve();},
  function(error) {defer.reject(error);},
  [arg1,arg2]
 );
 return defer.promise;
};

var method6 = function(){
 method1();
};
return{
method6:method6,
method4:method4
};
}]);

To test it, I have created spy object for all the services, but mentioning the problematic one

beforeEach( function() {
    someOtherService4Spy = jasmine.createSpyObj('someOtherService4', ['method4']); 
    someOtherService4Spy.method4.andCallFake(
        function(successCallback, errorCallback, data) {
           // var deferred = $q.defer();
            var error = function (errorCallback) { return error;}
            var success = function (successCallback) {
                deferred.resolve();
                return success;
            }
            return { success: success, error: error};
        }
    );

    module(function($provide) {
        $provide.value('someOtherService4', someOtherService4);
    });
    inject( function(_myService_, $injector, _$rootScope_,_$q_){
        myService = _myService_;
        $q = _$q_;
        $rootScope = _$rootScope_;
        deferred = _$q_.defer();
    });

});

it("test method6", function() {
    myService.method6();
    var expected  = expected;
    $rootScope.$digest();

    expect(someOtherService3.method3.mostRecentCall.args[0]).toEqualXml(expected);
    expect(someOtherService4Spy.method4).toHaveBeenCalledWith(jasmine.any(Function), jasmine.any(Function), [arg,arg]);
    expect(someOtherService4Spy.method4).toHaveBeenCalledWith(jasmine.any(Function), jasmine.any(Function), [arg,arg]);
});

It is showing error on

expect(someOtherService3.method3.mostRecentCall.args[0]).toEqualXml(expected);

After debugging I found that it is not waiting for any promise to resolve, so method 1 return true, without even executing method3. I even tried with

someOtherService4Spy.method4.andReturn(function(){return deferred.promise;});

But result remain same. My question is do I need to resolve multiple times ie for each promise. How can I wait till all the promises are executed.

Estus Flask
  • 206,104
  • 70
  • 425
  • 565
DRK
  • 191
  • 9

2 Answers2

0

method1 does not return the promise so how would you know the asynchrounous functions it calls are finished. Instead you should return:

return method2().then(

method6 calls asynchronous functions but again does not return a promise (it returns undefined) so how do you know it is finished? You should return:

return method1();

In a test you should mock $q and have it resolve or reject to a value but I can't think of a reason why you would have a asynchronous function that doesn't return anything since you won't know if it failed and when it's done.

Method 2 could be written in a more stable way because it would currently crash if the magically appearing myObject is empty (either {} or []

var method2 = function(){
  var defer = $q.defer();
  var keys = Object.keys(myObject);
  return keys.reduce(
    function(acc,item,index){
      return acc.then(
        function(){return method4(keys[index],myObject[key].data);},
        function(err){console.log("error calling method4:",err,key,myObject[key]);}
      )
    }
    ,$q.defer().resolve()
  )
};

And try not to have magically appearing variables in your function, this could be a global variable but your code does not show where it comes from and I doubt there is a need for this to be scoped outside your function(s) instead of passed to the function(s).

You can learn more about promises here you should understand why a function returns a promise (functions not block) and how the handlers are put on the queue. This would save you a lot of trouble in the future.

HMR
  • 37,593
  • 24
  • 91
  • 160
  • 1
    $q promises are supposed to be tested synchronously, no `done`. They heavily depend on $rootScope.$digest(). The code you've posted will likely result in test timeout. – Estus Flask Dec 20 '17 at 05:20
  • @estus Thank you for the information, the code posted by the OP doesn't seem to make any sense though, if you call method1 or method6 there is no way of knowing when they are done. Maybe in a test you could [mock](http://www.bradoncode.com/blog/2015/07/13/unit-test-promises-angualrjs-q/) `$q` but in production it doesn't seem to serve any purpose. – HMR Dec 20 '17 at 06:36
  • This is just a dummy code so didn't shared the whole code, due to which you can see some global var. I don't want method1 and method6 to return a promise but I agree with HMR that method 2 can be optimized, which I have done. – DRK Dec 21 '17 at 03:35
0

I did below modification to get it working. I was missing the handling of request to method5 due to which it was in hang state. Once I handled all the request to method 5 and provided successCallback (alongwith call to digest()), it started working.

    someOtherService4Spy.responseArray = {};
    someOtherService4Spy.requests = [];
    someOtherService4Spy.Method4.andCallFake( function(successCallback, errorCallback, data){

        var request = {data:data, successCallback: successCallback, errorCallback: errorCallback};
        someOtherService4Spy.requests.push(request);

        var error = function(errorCallback) {
            request.errorCallback = errorCallback;
        }
        var success = function(successCallback) {
            request.successCallback = successCallback;
            return {error: error};
        }

        return { success: success, error: error};
    });

    someOtherService4Spy.flush = function() {
        while(someOtherService4Spy.requests.length > 0) {
            var cachedRequests = someOtherService4Spy.requests;
            someOtherService4Spy.requests = [];
            cachedRequests.forEach(function (request) {
                if (someOtherService4Spy.responseArray[request.data[1]]) {
                    request.successCallback(someOtherService4Spy.responseArray[request.data[1]]);
                } else {
                    request.errorCallback(undefined);
                }
                $rootScope.$digest();
            });
        }
    }

Then I modified my test as :

    it("test method6", function() {
    myService.method6();

    var expected  = expected;
    var dataDict = {data1:"data1", data2:"data2"};
    for (var data in dataDict) {
        if (dataDict.hasOwnProperty(data)) {
            someOtherService4Spy.responseArray[dataDict[data]] = dataDict[data];
        }
    }

    someOtherService4Spy.flush();

    expect(someOtherService3.method3.mostRecentCall.args[0]).toEqualXml(expected);
    expect(someOtherService4Spy.method4).toHaveBeenCalledWith(jasmine.any(Function), jasmine.any(Function), [arg,arg]);

});

This worked as per my expectation. I was thinking that issue due to chain of promises but when I handled the method5 callback method, it got resolved. I got the idea of flushing of requests as similar thing I was doing for http calls.

DRK
  • 191
  • 9