Получение «$ digest уже выполняется» в асинхронном тестировании с Jasmine 2.0

#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 завершения каждого из них у него также есть дайджест. Итак, вот последовательность событий:

  1. вы вызываете flush() , который запускает дайджест
  2. then() выполняется
  3. done() выполняется
  4. 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();
});