36

我知道在摘要周期中调用$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 already in progress”错误的调用而爆炸。我不知道为什么。我可以通过包装done()像这样的超时来解决这个问题

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

这意味着done()将在 $digest 完成后排队并运行,但这解决了我想知道的问题

  • 为什么 Angular 首先处于摘要循环中?
  • 为什么调用done()会触发此错误?

我在 Jasmine 1.3 上运行了完全相同的测试,这只是在我升级到 Jasmine 2.0 并重写测试以使用新的 async-syntax 之后发生的。

4

3 回答 3

74

$httpBacked.flush()实际上开始并完成一个$digest()循环。昨天我花了一整天的时间研究 ngResource 和 angular-mocks 的源代码以深入了解它,但仍然没有完全理解它。

据我所知, 的目的$httpBackend.flush()是完全避免上面的异步结构。换句话说,it('should do something',function(done){});and的语法$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);
});

你会看到输出:

预先获得未定义

预冲洗未定义

异步回调开始 $digest

异步回调结束 $digest

冲洗后未定义

于 2014-10-08T08:09:04.120 回答
12

@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您想要运行的所有内容都将实际运行。

于 2015-10-23T19:27:07.390 回答
1

添加到@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();
});
于 2016-11-16T10:21:31.377 回答