Je sais que l'appel de $digest
ou $apply
manuellement au cours d'un cycle de résumé provoquera l'erreur "$ digest déjà en cours", mais je ne sais pas pourquoi je l'obtiens ici.
Il s'agit d'un test unitaire pour un service englobant $http
. Le service est assez simple. Il empêche simplement les appels en double au serveur tout en garantissant que le code qui tente d'effectuer les appels récupère toujours les données attendues.
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');
}
};
}]);
Le test unitaire est également assez simple, il utilise $httpBackend
pour attendre la demande.
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();
});
Cela explose de la même manière que done()
est appelé avec une erreur "$ digest déjà en cours". Je ne sais pas pourquoi. Je peux résoudre ce problème en enveloppant done()
dans une temporisation comme celle-ci.
setTimeout(function() { done() }, 1);
Cela signifie que done()
sera mis en file d'attente et fonctionnera une fois le $ digest terminé, mais pendant que cela résout mon problème, je veux savoir
done()
déclenche-t-il cette erreur?Jasmine 1.3 fonctionnait exactement au même test vert. Cela ne s’est produit que lorsque je suis passé à Jasmine 2.0 et que le test a été réécrit pour utiliser la nouvelle syntaxe asynchrone.
$httpBacked.flush()
démarre et termine réellement un cycle $digest()
. Hier, j'ai passé toute la journée à fouiller dans la source de ngResource et de moqueurs angulaires pour aller au fond des choses et je ne les comprends toujours pas bien.
Autant que je sache, le but de $httpBackend.flush()
est d'éviter complètement la structure async. En d'autres termes, les syntaxes de it('should do something',function(done){});
et $httpBackend.flush()
ne fonctionnent pas bien ensemble. Le même but de .flush()
consiste à effectuer un suivi des rappels asynchrones en attente, puis à y revenir. C'est comme une grande enveloppe done
autour de tous vos rappels asynchrones.
Donc, si j’ai bien compris (et que cela fonctionne pour moi maintenant), la bonne méthode consisterait à supprimer le processeur done()
lors de l’utilisation de $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();
});
Si vous ajoutez des instructions console.log, vous constaterez que tous les rappels ont lieu de manière cohérente au cours du cycle 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");
});
Ensuite, le résultat sera:
pré-get
pré-rinçage
rappel async commence
fin de rappel async
après la chasse
À chaque fois. Si vous voulez vraiment le voir, prenez l’étendue et regardez 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);
});
Et vous verrez la sortie:
pré-obtenir non défini
pré-rinçage non défini
rappel async commence par $ digérer
callback async end $ digest
post-chasse non définie
@deitch a raison, que $httpBacked.flush()
déclenche un condensé. Le problème est que, lorsque $httpBackend.verifyNoOutstandingExpectation();
est exécuté après chaque it
terminée, il contient également un condensé. Alors, voici la séquence d'événements:
flush()
qui déclenche un résuméthen()
est exécutédone()
est exécutéverifyNoOutstandingExpectation()
est exécuté, ce qui déclenche un condensé, mais vous en êtes déjà un et vous obtenez une erreur.done()
est toujours important car nous devons savoir que les 'attentes' dans le then()
sont même exécutées. Si la then
ne s'exécute pas, vous savez peut-être maintenant qu'il y a eu des échecs. La clé est de s'assurer que le résumé est complet avant de déclencher le 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();
});
Si vous mettez done()
dans un délai d'attente, celui-ci sera exécuté immédiatement après la fin du résumé actuel (). Cela garantira que tous les expects
que vous vouliez exécuter seront réellement exécutés.
Ajout à la réponse de @ deitch. Pour rendre les tests plus robustes, vous pouvez ajouter un espion avant votre rappel. Cela devrait garantir que votre rappel est effectivement appelé.
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();
});