Установка обработчика тайм-аута по обещанию в angularjs
Я пытаюсь установить тайм-аут в своем контроллере, чтобы, если ответ не получен в 250 мс, он должен выйти из строя. Я установил, чтобы мой unit test имел тайм-аут 10000, чтобы это условие было выполнено, может ли кто-нибудь указать мне в правильном направлении? (EDIT Я пытаюсь достичь этого, не используя службу $http, которую я знаю, обеспечивает функциональность тайм-аута)
(EDIT - мои другие юнит-тесты потерпели неудачу, потому что я не призывал к ним timeout.flush, теперь мне просто нужно получить сообщение о тайм-ауте, когда обещание undefined возвращается обещаниемService.getPromise(). Я удалил ранний код из вопроса).
promService (обещание - это переменная набора тестов, позволяющая мне использовать другое поведение для обещания в каждом наборе тестов до применения, например, отклонять в одном, успех в другом)
mockPromiseService = jasmine.createSpyObj('promiseService', ['getPromise']);
mockPromiseService.getPromise.andCallFake( function() {
promise = $q.defer();
return promise.promise;
})
Контроллер, который тестируется -
$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!");
});
}
Тест (обычно я разрешаю или отвергаю обещание здесь, но не устанавливая его, я имитирую таймаут)
it('Timeout logs promise failure', function(){
spyOn(console, 'log');
scope.qPromiseCall();
$timeout.flush(251);
$rootScope.$apply();
expect(console.log).toHaveBeenCalledWith("Timed out");
})
Ответы
Ответ 1
Во-первых, я хотел бы сказать, что реализация вашего контроллера должна быть примерно такой:
$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
});
}
Ваши тесты:
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
Другой пример с 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
}
});
Ваши тесты аналогичны приведенному выше примеру:
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
Ответ 2
Поведение "отказ от обещания, если оно не разрешено с заданным таймфреймом", кажется идеальным для реорганизации в отдельную службу / factory. Это должно сделать код как в новом сервисе /factory, так и в контроллере более четким и более повторно используемым.
Контроллер, который я предположил, просто устанавливает успех/сбой в области:
app.controller('MainCtrl', function($scope, failUnlessResolvedWithin, myPromiseService) {
failUnlessResolvedWithin(function() {
return myPromiseService.getPromise();
}, 250).then(function(result) {
$scope.result = result;
}, function(error) {
$scope.error = error;
});
});
И factory, failUnlessResolvedWithin
создает новое обещание, которое эффективно "перехватывает" обещание от переданной функции. Он возвращает новый, который реплицирует поведение разрешения/отклонения, за исключением того, что он также отвергает обещание, если оно не было разрешено в течение таймаута:
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;
};
});
Тесты для них немного сложны (и длинны), но вы можете видеть их в http://plnkr.co/edit/3e4htwMI5fh595ggZY7h?p=preview. Основные моменты тестов:
-
Тесты для контроллера mocks failUnlessResolvedWithin
с вызовом $timeout
.
$provide.value('failUnlessResolvedWithin', function(func, time) {
return $timeout(func, time);
});
Это возможно, поскольку "failUnlessResolvedWithin" (намеренно) синтаксически эквивалентен $timeout
и выполняется, поскольку $timeout
предоставляет функцию flush
для проверки различных случаев.
-
Тесты для самой службы используют вызовы $timeout.flush
для проверки поведения различных случаев первоначального обещания, которое разрешается/отклоняется до/после таймаута.
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');
});
Вы можете увидеть все это в действии на http://plnkr.co/edit/3e4htwMI5fh595ggZY7h?p=preview
Ответ 3
Моя реализация @Michal Charemza failUnlessResolvedWithin с реальной выборкой.
Передавая отложенный объект функции func, он уменьшает необходимость создания обещания в коде использования "ByUserPosition". Помогает мне справиться с firefox и геолокацией.
.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);
});
};