it-swarm.com.de

Festlegen eines Timeout-Handlers für ein Versprechen in anglejs

Ich versuche, eine Zeitüberschreitung in meinem Controller festzulegen, damit eine Antwort in 250ms nicht möglich ist. Ich habe für den Gerätetest ein Timeout von 10000 festgelegt, damit diese Bedingung erfüllt sein kann. Kann mir jemand die richtige Richtung weisen? (BEARBEITEN Ich versuche, dies ohne den $ http-Dienst zu erreichen, von dem ich weiß, dass er Timeout-Funktionalität bietet. 

(BEARBEITEN - meine anderen Unit-Tests sind fehlgeschlagen, weil ich nicht mit timeout angerufen wurde früher Code aus der Frage).

promiseService (promise ist eine Testsuite-Variable, die es mir ermöglicht, vor jeder Anwendung ein anderes Verhalten für das Versprechen in jeder Testsuite zu verwenden, z. B. ablehnen in einer, Erfolg in einer anderen)

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

Controller-Funktion, die getestet wird - 

$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 (normalerweise löse oder lehne ich das Versprechen hier ab, simuliere jedoch ein Timeout)

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

Zunächst möchte ich sagen, dass Ihre Controller-Implementierung so aussehen sollte:

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

Ihre 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

Ein weiteres Beispiel mit 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

  }
});

Ihre Tests ähneln dem obigen Beispiel:

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

Das Verhalten des Versagens eines Versprechens, wenn es nicht mit einem bestimmten Zeitrahmen gelöst wird, scheint ideal für die Umgestaltung in einen separaten Service/eine andere Fabrik. Dadurch sollte der Code sowohl im neuen Service/Werk als auch im Controller klarer und wiederverwendbarer werden.

Der Controller, von dem ich ausgegangen bin, setzt nur den Erfolg/Misserfolg im Umfang:

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

Und die Fabrik, failUnlessResolvedWithin, schafft ein neues Versprechen, das ein Versprechen einer übergebenen Funktion effektiv "abfängt". Es wird ein neues zurückgegeben, das sein Verhalten zum Beheben/Ablehnen repliziert, mit der Ausnahme, dass es auch das Versprechen ablehnt, wenn es nicht innerhalb des Timeouts gelöst wurde:

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

Die Tests für diese sind etwas schwierig (und langwierig), aber Sie können sie unter http://plnkr.co/edit/3e4htwMI5fh595ggZY7h?p=preview sehen. Die Hauptpunkte der Tests sind

  • Die Tests für den Controller mockten failUnlessResolvedWithin mit einem Aufruf von $timeout.

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

    Dies ist möglich, da 'failUnlessResolvedWithin' (absichtlich) syntaktisch äquivalent zu $timeout ist und ausgeführt wird, da $timeout die Funktion flush zum Testen verschiedener Fälle bereitstellt.

  • Die Tests für den Dienst selbst verwenden Aufrufe $timeout.flush, um das Verhalten der verschiedenen Fälle zu testen, in denen das ursprüngliche Versprechen vor/nach dem Timeout aufgelöst/abgelehnt wurde.

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

Sie können all dies in Aktion unter http://plnkr.co/edit/3e4htwMI5fh595ggZY7h?p=preview sehen.

8
Michal Charemza

Meine Implementierung von @Michal Charemza 'failUnlessResolvedWithin mit einem echten Sample . Durch das Übergeben eines zurückgestellten Objekts an die Funktion reduziert es die Notwendigkeit, ein Versprechen im Verwendungscode "ByUserPosition" zu instantiieren. Hilft mir beim Umgang mit Firefox und 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);
                });
            };
1
Leblanc Meneses