Подтвердить что ты не робот

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

Я знаю, что вызов $digest или $apply вручную во время цикла дайджеста приведет к ошибке "$ digest уже в процессе", но я понятия не имею, почему я получаю его здесь.

Это unit test для службы, которая обертывает $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();
});

Это сработает как sone, так как done() вызывается с ошибкой "$ digest is in progress". Я понятия не имею, почему. Я могу решить это, обернув done() в таймаут, подобный этому

setTimeout(function() { done() }, 1);

Это означает, что done() будет стоять в очереди и запускаться после выполнения $digest, но пока это решает мою проблему, я хочу знать

  • Почему Angular в цикле дайджеста в первую очередь?
  • Почему вызов done() вызывает эту ошибку?

У меня был тот же самый тест, который работал зеленым с Jasmine 1.3, это произошло только после того, как я обновился до Jasmine 2.0 и переписал тест, чтобы использовать новый синтаксис асинхронизации.

4b9b3361

Ответ 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();
});

Если вы добавите инструкции console.log, вы обнаружите, что во время цикла 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");
});

Тогда вывод будет:

предварительно прибудет

для предварительной промывки

начало асинхронного вызова

конец асинхронного обратного вызова

после смыва

Каждый раз. Если вы действительно хотите это увидеть, возьмите область и посмотрите 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);
});

И вы увидите результат:

pre-get undefined

pre-flush undefined

async callback begin $digest

асинхронный конец обратного вызова $digest

пост-флеш undefined

Ответ 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, которые вы хотите запустить, будут выполняться.

Ответ 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();
});