18

I'm trying to set a timeout in my controller so that if a response isn't received in 250ms it should fail. I've set my unit test to have a timeout of 10000 so that this condition should be met,Can anyone point me in the right direction? ( EDIT I'm trying to achieve this without using the $http service which I know provides timeout functinality)

(EDIT - my other unit tests were failing because I wasn't calling timeout.flush on them, now I just need to get the timeout message kicking in when an undefined promise is returned by promiseService.getPromise(). I've removed the early code from the question) .

promiseService (promise is a test suite variable allowing me to use different behaviour for the promise in each test suite before apply, eg reject in one, success in another)

    mockPromiseService = jasmine.createSpyObj('promiseService', ['getPromise']);
    mockPromiseService.getPromise.andCallFake( function() {
        promise = $q.defer();
        return promise.promise;
    })

Controller function that's being tested -

$scope.qPromiseCall = function() {
    var timeoutdata = null;
    $timeout(function() {
        promise = promiseService.getPromise();
        promise.then(function (data) {
                timeoutdata = data;
                if (data == "promise success!") {
                    console.log("success");
                } else {
                    console.log("function failure");
                }
            }, function (error) {
                console.log("promise failure")
            }

        )
    }, 250).then(function (data) {
        if(typeof timeoutdata === "undefined" ) {
            console.log("Timed out")
        }
    },function( error ){
        console.log("timed out!");
    });
}

Test (normally I resolve or reject the promise in here but by not setting it I'm simulating a timeout)

it('Timeout logs promise failure', function(){
    spyOn(console, 'log');
    scope.qPromiseCall();
    $timeout.flush(251);
    $rootScope.$apply();
    expect(console.log).toHaveBeenCalledWith("Timed out");
})
7
  • Can you show us promiseService.getPromise()? Commented Apr 10, 2014 at 19:01
  • It's not implemented yet I'm trying to get this designed first, should it be linked to the implementation of the promise service? Commented Apr 10, 2014 at 19:20
  • 1
    How can you tell it's not working if you haven't implemented it yet Commented Apr 10, 2014 at 19:29
  • It's a unit test so I'm injecting the promiseService.getPromise... as I type this I realise I don't have the inject code, adding now sry Commented Apr 10, 2014 at 19:48
  • I don't see the number '250' anywhere in your code there... how do you expect it to do something after 250 ms? Commented Apr 11, 2014 at 7:07

3 Answers 3

31
+150

First, I would like to say that your controller implementation should be something like this:

$scope.qPromiseCall = function() {

    var timeoutPromise = $timeout(function() {
      canceler.resolve(); //aborts the request when timed out
      console.log("Timed out");
    }, 250); //we set a timeout for 250ms and store the promise in order to be cancelled later if the data does not arrive within 250ms

    var canceler = $q.defer();
    $http.get("data.js", {timeout: canceler.promise} ).success(function(data){
      console.log(data);

      $timeout.cancel(timeoutPromise); //cancel the timer when we get a response within 250ms
    });
  }

Your tests:

it('Timeout occurs', function() {
    spyOn(console, 'log');
    $scope.qPromiseCall();
    $timeout.flush(251); //timeout occurs after 251ms
    //there is no http response to flush because we cancel the response in our code. Trying to  call $httpBackend.flush(); will throw an exception and fail the test
    $scope.$apply();
    expect(console.log).toHaveBeenCalledWith("Timed out");
  })

  it('Timeout does not occur', function() {
    spyOn(console, 'log');
    $scope.qPromiseCall();
    $timeout.flush(230); //set the timeout to occur after 230ms
    $httpBackend.flush(); //the response arrives before the timeout
    $scope.$apply();
    expect(console.log).not.toHaveBeenCalledWith("Timed out");
  })

DEMO

Another example with promiseService.getPromise:

app.factory("promiseService", function($q,$timeout,$http) {
  return {
    getPromise: function() {
      var timeoutPromise = $timeout(function() {
        console.log("Timed out");

        defer.reject("Timed out"); //reject the service in case of timeout
      }, 250);

      var defer = $q.defer();//in a real implementation, we would call an async function and 
                             // resolve the promise after the async function finishes

      $timeout(function(data){//simulating an asynch function. In your app, it could be
                              // $http or something else (this external service should be injected
                              //so that we can mock it in unit testing)
        $timeout.cancel(timeoutPromise); //cancel the timeout 

         defer.resolve(data);
      });

      return defer.promise;
    }
  };
});

app.controller('MainCtrl', function($scope, $timeout, promiseService) {

  $scope.qPromiseCall = function() {

    promiseService.getPromise().then(function(data) {
      console.log(data); 
    });//you could pass a second callback to handle error cases including timeout

  }
});

Your tests are similar to the above example:

it('Timeout occurs', function() {
    spyOn(console, 'log');
    spyOn($timeout, 'cancel');
    $scope.qPromiseCall();
    $timeout.flush(251); //set it to timeout
    $scope.$apply();
    expect(console.log).toHaveBeenCalledWith("Timed out");
  //expect($timeout.cancel).not.toHaveBeenCalled(); 
  //I also use $timeout to simulate in the code so I cannot check it here because the $timeout is flushed
  //In real app, it is a different service
  })

it('Timeout does not occur', function() {
    spyOn(console, 'log');
    spyOn($timeout, 'cancel');
    $scope.qPromiseCall();
    $timeout.flush(230);//not timeout
    $scope.$apply();
    expect(console.log).not.toHaveBeenCalledWith("Timed out");
    expect($timeout.cancel).toHaveBeenCalled(); //also need to check whether cancel is called
  })

DEMO

Sign up to request clarification or add additional context in comments.

6 Comments

Thanks unfortunately I accidentally edited the part of my question which requires this to be for a promise not handled by $http service as I know that has a built-in timeout handler, would you be able to update for this scenario?
@LinuxN00b: did you look at my second example? Does it meet your requirement?
Thanks you I'm still having my morning coffee so I completely missed the second demo. In this example, in real life, how would you go about handling the timeout for the promiseService promise in a way that's clean and prevents the promise from returning later at an unexpected time and triggering an issue?
@LinuxN00b: check my updated answer to see it you're satisfied. In this example, I move all of the code to one function. This is just a demo of the idea. The keys are rejecting the promise if timeout occurs and cancelling timeout if the response arrives before the timeout.
Sorry I didn't realise you had to click to also award the bounty I was satisfied anyway. Edit - it's saying 3 hours until I can accept, will do so then, thank you very much
|
9

The behaviour of "failing a promise unless it is resolved with a specified timeframe" seems ideal for refactoring into a separate service/factory. This should make the code in both the new service/factory and controller clearer and more re-usable.

The controller, which I've assumed just sets the success/failure on the scope:

app.controller('MainCtrl', function($scope, failUnlessResolvedWithin, myPromiseService) {
  failUnlessResolvedWithin(function() {
    return myPromiseService.getPromise();
  }, 250).then(function(result) {
    $scope.result = result;
  }, function(error) {
    $scope.error = error;
  });
});

And the factory, failUnlessResolvedWithin, creates a new promise, which effectively "intercepts" a promise from a passed in function. It returns a new one that replicates its resolve/reject behaviour, except that it also rejects the promise if it hasn't been resolved within the timeout:

app.factory('failUnlessResolvedWithin', function($q, $timeout) {

  return function(func, time) {
    var deferred = $q.defer();

    $timeout(function() {
      deferred.reject('Not resolved within ' + time);
    }, time);

    $q.when(func()).then(function(results) {
      deferred.resolve(results);
    }, function(failure) {
      deferred.reject(failure);
    });

    return deferred.promise;
  };
});

The tests for these are a bit tricky (and long), but you can see them at http://plnkr.co/edit/3e4htwMI5fh595ggZY7h?p=preview . The main points of the tests are

  • The tests for the controller mocks failUnlessResolvedWithin with a call to $timeout.

    $provide.value('failUnlessResolvedWithin', function(func, time) {
      return $timeout(func, time);
    });
    

    This is possible since 'failUnlessResolvedWithin' is (deliberately) syntactically equivalent to $timeout, and done since $timeout provides the flush function to test various cases.

  • The tests for the service itself uses calls $timeout.flush to test behaviour of the various cases of the original promise being resolved/rejected before/after the timeout.

    beforeEach(function() {
      failUnlessResolvedWithin(func, 2)
      .catch(function(error) {
        failResult = error;
      });
    });
    
    beforeEach(function() {
      $timeout.flush(3);
      $rootScope.$digest();
    });
    
    it('the failure callback should be called with the error from the service', function() {
      expect(failResult).toBe('Not resolved within 2');
    });   
    

You can see all this in action at http://plnkr.co/edit/3e4htwMI5fh595ggZY7h?p=preview

5 Comments

Thanks unfortunately I accidentally edited the part of my question which requires this to be for a promise not handled by $http service as I know that has a built-in timeout handler, would you be able to update for this scenario?
I don't think my answer requires anything relating to the $http service. The getPromise can be any function that returns a promise. Can you clarify why you think it does? Or if I'm misunderstanding, clarify your requirements in the question.
The answer feeds the timeout to the $http call which handles the timeout doesn't it?
I'm not sure what you mean by feeding, but the only Angular services I'm using are $q and $timeout. Neither of them then call anything from $http (as far as I know).
@MichalCharemza +1 I added an answer that is a derivative of your original answer.
1

My implementation of @Michal Charemza 's failUnlessResolvedWithin with a real sample. By passing deferred object to the func it reduces having to instantiate a promise in usage code "ByUserPosition". Helps me deal with firefox and geolocation.

.factory('failUnlessResolvedWithin', ['$q', '$timeout', function ($q, $timeout) {

    return function(func, time) {
        var deferred = $q.defer();

        $timeout(function() {
            deferred.reject('Not resolved within ' + time);
        }, time);

        func(deferred);

        return deferred.promise;
    }
}])



            $scope.ByUserPosition = function () {
                var resolveBy = 1000 * 30;
                failUnlessResolvedWithin(function (deferred) {
                    navigator.geolocation.getCurrentPosition(
                    function (position) {
                        deferred.resolve({ latitude: position.coords.latitude, longitude: position.coords.longitude });
                    },
                    function (err) {
                        deferred.reject(err);
                    }, {
                        enableHighAccuracy : true,
                        timeout: resolveBy,
                        maximumAge: 0
                    });

                }, resolveBy).then(findByPosition, function (data) {
                    console.log('error', data);
                });
            };

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.