在Jasmine 2.0的asynchronoustesting中获得“$摘要已经在进行中”

我知道,在摘要循环中调用$digest$apply程序将导致“$摘要已在进行中”错误,但我不知道为什么我在这里得到它。

这是一个包装$http的服务的unit testing,服务非常简单,它只是防止重复调用服务器,同时确保尝试执行调用的代码仍然获得所需的数据。

 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'); } }; }]); 

unit testing也非常简单,它使用$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()会被调用“$摘要已在进行中”错误。 我不知道为什么。 我可以通过在这样的超时中包装done()来解决这个问题

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

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

  • 为什么angular度在消化循环中首先?
  • 为什么调用done()触发这个错误?

我有与Jasmine 1.3运行绿色完全相同的testing,这只发生在我升级到茉莉花2.0后,并重写testing使用新的asynchronous语法。

$httpBacked.flush()实际上启动并完成一个$digest()循环。 我昨天花了整天的时间挖掘ngResource和angular-mock的源头,深入了解了这一点,但仍然没有完全理解它。

据我所知, $httpBackend.flush()的目的是完全避免上面的asynchronous结构。 换句话说, it('should do something',function(done){});的语法it('should do something',function(done){});$httpBackend.flush()不能很好地一起玩。 .flush() 目的是通过待处理的asynchronouscallback,然后返回。 这就像一个大包装done你所有的asynchronouscallback。

所以,如果我理解正确(现在适用于我),正确的方法是使用$httpBackend.flush()时删除done()处理器:

 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语句,你会发现所有的callback在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"); }); 

那么输出将是:

预取

冲洗前

asynchronouscallback开始

asynchronouscallback结束

后冲水

每次。 如果你真的想看到它,抓住范围,看看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); }); 

你会看到输出:

预先获取undefined

预冲洗undefined

asynchronouscallback开始$摘要

asynchronouscallback结束$摘要

后冲洗undefined

@deitch是正确的, $httpBacked.flush()触发摘要。 问题是,当$httpBackend.verifyNoOutstandingExpectation();it完成之后运行,它也有一个摘要。 所以这是事件的顺序:

  1. 你调用flush()触发一个摘要
  2. then()被执行
  3. done()被执行
  4. 运行verifyNoOutstandingExpectation()会触发一个摘要,但是你已经在一个,所以你得到一个错误。

done()仍然很重要,因为我们需要知道then()中的'expect'是否被执行。 如果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将实际运行。

添加@ deitch的答案。 为了使testing更健壮,可以在callback之前添加一个间谍。 这应该保证你的callback实际上被调用。

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