#angularjs #jasmine #jasmine2.0
#angularjs #jasmine #jasmine2.0
Вопрос:
Я знаю, что вызов $digest
или $apply
вручную во время цикла дайджеста вызовет ошибку «$ digest уже выполняется», но я понятия не имею, почему я получаю ее здесь.
Это модульный тест для службы, которая завершает $http
, служба достаточно проста, она просто предотвращает повторные вызовы на сервер, гарантируя, что код, который пытается выполнить вызовы, все равно получает ожидаемые данные.
angular.module('services')
.factory('httpService', ['$http', function($http) {
var pendingCalls = {};
var createKey = function(url, data, method) {
return method url JSON.stringify(data);
};
var send = function(url, data, method) {
var key = createKey(url, data, method);
if (pendingCalls[key]) {
return pendingCalls[key];
}
var promise = $http({
method: method,
url: url,
data: data
});
pendingCalls[key] = promise;
promise.then(function() {
delete pendingCalls[key];
});
return promise;
};
return {
post: function(url, data) {
return send(url, data, 'POST');
},
get: function(url, data) {
return send(url, data, 'GET');
},
_delete: function(url, data) {
return send(url, data, 'DELETE');
}
};
}]);
Модульный тест также довольно прост, он используется $httpBackend
для ожидания запроса.
it('does GET requests', function(done) {
$httpBackend.expectGET('/some/random/url').respond('The response');
service.get('/some/random/url').then(function(result) {
expect(result.data).toEqual('The response');
done();
});
$httpBackend.flush();
});
Это происходит сразу же done()
после вызова с ошибкой «$ digest уже выполняется». Я понятия не имею, почему. Я могу решить эту проблему, установив тайм done()
-аут следующим образом
setTimeout(function() { done() }, 1);
Это означает done()
, что он будет помещен в очередь и запущен после завершения $ digest, но пока это решает мою проблему, я хочу знать
- Почему Angular в цикле дайджеста в первую очередь?
- Почему вызов
done()
вызывает эту ошибку?
У меня был точно такой же тест, запущенный зеленым цветом с Jasmine 1.3, это произошло только после того, как я обновился до Jasmine 2.0 и переписал тест, чтобы использовать новый асинхронный синтаксис.
Комментарии:
1. Это новый способ работы с асинхронными тестами в Jasmine 2.0. Он вводится в тестовую функцию, и если вы не вызвали его в течение 5 секунд, тест завершается неудачей. Смотрите jasmine.github.io/2.0 /…
2. Для jasmine 2.0 не было отдельного тега, иначе я бы пометил его. Я вижу, как сбивает с толку синтаксис, если вы его раньше не видели.
3. Хорошо, вчера я потратил на это почти весь день, но я думаю, что понял это. Я добавлю это в качестве ответа, и я могу ошибаться.
4. @deitch это похоже на хорошее исследование, я посмотрю, подтвержду ли я то, что вы нашли.
5. @ivarni пожалуйста, сделайте и опубликуйте то, что найдете.
Ответ №1:
$httpBacked.flush()
фактически запускает и завершает $digest()
цикл. Вчера я весь день копался в источниках ngResource и angular-mocks, чтобы разобраться в этом, и до сих пор не до конца понимаю это.
Насколько я могу судить, цель $httpBackend.flush()
состоит в том, чтобы полностью избежать асинхронной структуры, описанной выше. Другими словами, синтаксис it('should do something',function(done){});
и $httpBackend.flush()
не очень хорошо сочетаются друг с другом. Сама цель .flush()
состоит в том, чтобы протолкнуть ожидающие асинхронные обратные вызовы и затем вернуться. Это похоже на одну большую done
оболочку вокруг всех ваших асинхронных обратных вызовов.
Итак, если я правильно понял (и это работает для меня сейчас), правильным методом было бы удалить done()
процессор при использовании $httpBackend.flush()
:
it('does GET requests', function() {
$httpBackend.expectGET('/some/random/url').respond('The response');
service.get('/some/random/url').then(function(result) {
expect(result.data).toEqual('The response');
});
$httpBackend.flush();
});
Если вы добавите консоль.запишите инструкции, вы обнаружите, что все обратные вызовы последовательно выполняются в течение flush()
цикла:
it('does GET requests', function() {
$httpBackend.expectGET('/some/random/url').respond('The response');
console.log("pre-get");
service.get('/some/random/url').then(function(result) {
console.log("async callback begin");
expect(result.data).toEqual('The response');
console.log("async callback end");
});
console.log("pre-flush");
$httpBackend.flush();
console.log("post-flush");
});
Тогда результат будет:
pre-get
предварительная очистка
начинается асинхронный обратный вызов
завершение асинхронного обратного вызова
пост-флеш
Каждый раз. Если вы действительно хотите это увидеть, возьмите область и посмотрите scope.$$phase
var scope;
beforeEach(function(){
inject(function($rootScope){
scope = $rootScope;
});
});
it('does GET requests', function() {
$httpBackend.expectGET('/some/random/url').respond('The response');
console.log("pre-get " scope.$$phase);
service.get('/some/random/url').then(function(result) {
console.log("async callback begin " scope.$$phase);
expect(result.data).toEqual('The response');
console.log("async callback end " scope.$$phase);
});
console.log("pre-flush " scope.$$phase);
$httpBackend.flush();
console.log("post-flush " scope.$$phase);
});
И вы увидите результат:
предварительно получить неопределенный
предварительная очистка не определена
асинхронный обратный вызов запускает $digest
асинхронный обратный вызов завершает $ digest
пост-сброс не определен
Комментарии:
1. Да, похоже, это работает, и, по крайней мере, для меня объяснение также имеет смысл. Спасибо!
2. Если бы я мог проголосовать за это более одного раза, я бы это сделал. Это спасло меня от копания в источнике после того, как я все утро бился об это головой. Приветствия!
3. Месяцы спустя, и все же вы сделали мой день. Я действительно рад, что все эти работы помогли другим.
4. Вау, это сводило меня с ума сегодня днем. Еще раз, документы angular меня подвели. Возможно, ответ там, но это не очевидно. Спасибо за отличный ответ!
5. @markrian отлично! Меня мало беспокоит больше, чем 2 человека, выполняющих одну и ту же работу. Я боролся с этим, и теперь мои усилия были полезны для вас. Это хороший день в моей книге.
Ответ №2:
@deitch прав, это $httpBacked.flush()
запускает дайджест. Проблема в том, что при $httpBackend.verifyNoOutstandingExpectation();
запуске после it
завершения каждого из них у него также есть дайджест. Итак, вот последовательность событий:
- вы вызываете
flush()
, который запускает дайджест then()
выполняетсяdone()
выполняетсяverifyNoOutstandingExpectation()
выполняется, который запускает дайджест, но вы уже в нем, поэтому получаете сообщение об ошибке.
done()
по-прежнему важно, поскольку нам нужно знать, что «ожидания» внутри then()
даже выполняются. Если then
он не запускается, теперь вы можете знать, что были сбои. Ключ в том, чтобы убедиться, что дайджест завершен, прежде чем запускать done()
.
it('does GET requests', function(done) {
$httpBackend.expectGET('/some/random/url').respond('The response');
service.get('/some/random/url').then(function(result) {
expect(result.data).toEqual('The response');
setTimeout(done, 0); // run the done() after the current $digest is complete.
});
$httpBackend.flush();
});
Ввод done()
тайм-аута приведет к его выполнению сразу после завершения текущего дайджеста (). Это гарантирует, что все expects
, что вы хотели запустить, действительно будет запущено.
Комментарии:
1.
setTimeout
Решение было тем, чего я пытался избежать в первую очередь 🙂2. Вы можете поместить verifyNoOutstandingExpectation() в тайм-аут в вашем afterEach, чтобы вместо этого сделать ваш «it» более чистым.
Ответ №3:
Добавление к ответу @deitch. Чтобы сделать тесты более надежными, вы можете добавить шпиона перед обратным вызовом. Это должно гарантировать, что ваш обратный вызов действительно будет вызван.
it('does GET requests', function() {
var callback = jasmine.createSpy().and.callFake(function(result) {
expect(result.data).toEqual('The response');
});
$httpBackend.expectGET('/some/random/url').respond('The response');
service.get('/some/random/url').then(callback);
$httpBackend.flush();
expect(callback).toHaveBeenCalled();
});