web-dev-qa-db-fra.com

Définition d'un gestionnaire de délai d'attente sur une promesse en angularjs

J'essaie de définir un délai d'expiration dans mon contrôleur afin que, si une réponse n'est pas reçue dans un délai de 250 ms, elle échoue. J'ai configuré mon test d'unité sur un délai d'expiration de 10 000 pour que cette condition soit remplie. Quelqu'un peut-il m'indiquer la bonne direction? (EDIT j'essaye d'y parvenir sans utiliser le service $ http qui, je le sais, fournit une fonctionnalité de délai d'attente) 

(EDIT - mes autres tests unitaires échouaient parce que je n’appelais pas timeout.flush sur eux. Maintenant, j’ai juste besoin que le message de délai expire quand une promesse non définie est renvoyée par promiseService.getPromise (). code précoce de la question).

promiseService (promise est une variable de la suite de tests me permettant d'utiliser un comportement différent pour la promesse dans chaque suite de tests avant de l'appliquer, par exemple, rejeter dans l'une, succès dans une autre)

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

Fonction de contrôleur qui est en cours de test - 

$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 (normalement, je résous ou rejette la promesse ici, mais en ne la définissant pas, je simule un dépassement de délai)

it('Timeout logs promise failure', function(){
    spyOn(console, 'log');
    scope.qPromiseCall();
    $timeout.flush(251);
    $rootScope.$apply();
    expect(console.log).toHaveBeenCalledWith("Timed out");
})
16
SMC

Tout d’abord, je voudrais dire que l’implémentation de votre contrôleur devrait ressembler à ceci:

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

Vos 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

Un autre exemple avec 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

  }
});

Vos tests sont similaires à l'exemple ci-dessus:

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

30
Khanh TO

Le comportement consistant à "échouer une promesse à moins que celle-ci ne soit résolue dans un délai déterminé" semble idéal pour la refactorisation dans un service/une usine distincte. Cela devrait rendre le code à la fois dans le nouveau service/usine et dans le contrôleur plus clair et réutilisable.

Le contrôleur, que j'ai supposé juste définit le succès/échec sur la portée:

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

Et l'usine, failUnlessResolvedWithin, crée une nouvelle promesse, qui "intercepte" efficacement une promesse d'une fonction passée. Il en retourne un nouveau qui reproduit son comportement de résolution/rejet, sauf qu'il rejette également la promesse si elle n'a pas été résolue dans le délai imparti:

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

Les tests pour ceux-ci sont un peu difficiles (et longs), mais vous pouvez les voir à http://plnkr.co/edit/3e4htwMI5fh595ggZY7h?p=preview . Les points principaux des tests sont

  • Les tests du contrôleur simulent failUnlessResolvedWithin avec un appel à $timeout.

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

    Cela est possible car 'failUnlessResolvedWithin' est (délibérément) syntaxiquement équivalent à $timeout et que $timeout fournit la fonction flush pour tester divers cas.

  • Les tests du service lui-même utilisent les appels $timeout.flush pour tester le comportement des différents cas de résolution/de rejet de la promesse d'origine avant/après le délai d'expiration.

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

Vous pouvez voir tout cela en action à http://plnkr.co/edit/3e4htwMI5fh595ggZY7h?p=preview

8
Michal Charemza

Mon implémentation de failUnlessResolvedWithin de @Michal Charemza avec un échantillon réel . En transmettant un objet différé à la fonction, cela évite d'avoir à instancier une promesse dans le code d'utilisation "ByUserPosition". M'aide à gérer firefox et la géolocalisation.

.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);
                });
            };
1
Leblanc Meneses