web-dev-qa-db-fra.com

Obtenir "$ digest déjà en cours" dans un test asynchrone avec Jasmine 2.0

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 

  • Pourquoi Angular est-il en premier lieu dans un cycle de digestion?
  • Pourquoi l'appel 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.

35
ivarni

$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

73
deitch

@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:

  1. vous appelez flush() qui déclenche un résumé
  2. le then() est exécuté
  3. le done() est exécuté
  4. 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.

12
Matt Slocum

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();
});
1
Janne Annala