使用 Jasmine 的 done() 做异步测试?

Using Jasmine's done() to do asynchronous testing?

我正在尝试使用 Jasmine 测试一些异步 JavaScript(曾经是 TypeScript)。我很难让它正常工作,并且通过这个简单的示例它永远不会进入 then(function( 代码块并且我收到以下错误:

Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.

我的测试看起来像:

it("Should work", function(done){
    dataService.ready = true;
    dataService.isReady().then(function(result){
        console.log(result);
        expect(result).toBe(true);
        done();
    });
});

我正在测试的服务看起来像(在编译为 JavaScript 之前):

public isReady(): angular.IPromise<any> {

   var deferred = this.q.defer();

   if (this.ready) {
       setTimeout(() => { return deferred.resolve(true); }, 1);
   } else {
       // a bunch of other stuff that eventually returns a promise
   }

   return deferred.promise;
}

我确定我只是误用了 done() 但我觉得这应该有效!有什么建议吗?


更新:

为了进一步调试,我在 isReady() 函数中添加了一些控制台日志。现在看起来像:

public isReady(): angular.IPromise<any> {

   var deferred = this.q.defer();
   console.log("in isReady()"); // new line to add logging

   if (this.ready) {
       console.log("this.ready is true"); // new line to add logging
       setTimeout(() => {
            console.log("returning deferred.resolve"); // new line to add logging
            return deferred.resolve(true);
       }, 1);
   } else {
       // a bunch of other stuff that eventually returns a promise
   }

   return deferred.promise;
}
当我在浏览器中手动测试时,

isReady() 按预期工作。 运行测试时,我的日志包括:

LOG: 'in isReady()'
LOG: 'this.ready is true'
LOG: 'returning deferred.resolve'

在我的测试中,它似乎永远不会被解决(then() 中的代码块从未被执行)但是当 运行 我的应用程序这个功能工作得很好。这个例子在一个控制器中:

DataService.isReady().then(() => {
   console.log("I work!");
});

更新:还有更多调试...

在我的测试中:

it("Should work", function(done){
    console.log("calling dataService.isReady()");
    var prom = dataService.isReady();

    console.log("promise before");
    console.log(prom);

    setTimeout(function(){
        console.log("promise after");
        console.log(prom);
     },1000);

     prom.then(function(result){
         // never makes it here
         done();
     }, function(reason) {
        // never makes it here either
     });
}

现在,在我的控制台中,我看到:

LOG: 'calling dataService.isReady()'
LOG: 'in isReady()'
LOG: 'this.ready is true'
LOG: 'promise before'
LOG: Object{$$state: Object{status: 0}}
LOG: 'returning deferred.resolve'
LOG: 'promise after'
LOG: Object{$$state: Object{status: 1, pending: [...], value: true, processScheduled: true}}

所以,我的承诺看起来应该如此。为什么 then() 没有被调用?

实际发生了什么: 所以,事实证明我的问题应该是 "Why doesn't my angular promise resolve in my Jasmine test?"


$摘要

经过一番挖掘和查看其他解决方案后,我发现了一些关于 when/how promises 已解决的有用信息。我需要在 $rootScope 上调用 $digest() 以解决承诺并执行 then() 代码块(因此调用 done() 以满足规范)。

没有更多的请求预期错误

添加 $rootScope.$digest() 让我完成了 most,但后来我开始发现一个 No more request expected 错误导致我的测试失败。这是因为我正在使用的服务正在为我的应用程序的另一个方面发送各种 POSTGET 请求。删除 whenGETwhenPOST 回复似乎解决了这个问题。


最终解决方案:

长话短说,我的规范文件现在看起来像:

describe("Async Tests", function(){

    var dataService;
    var rootScope;
    var httpBackend;

    beforeEach(module("myangularapp"));

    beforeEach(inject(function(_$httpBackend_, _DataService_, $rootScope){

        dataService = _DataService_;
        rootScope = $rootScope;
        httpBackend = _$httpBackend_;

        // solves the 'No more request expected' errors:
        httpBackend.whenGET('').respond([]);
        httpBackend.whenPOST('').respond([]);

    }));

    it("Should work", function(done){

        dataService.ready = true;

        dataService.isReady().then(function(result){

            console.log(result);
            expect(result).toBe(true);

            // still calls done() just as before
            done();

        });

        // digest the scope every so often so we can resolve the promise from the DataService isReady() function
        setInterval(rootScope.$digest, 100);

    });

});

这个解决方案似乎比它需要的更复杂,但我认为它现在可以解决问题。我希望这可以帮助任何其他可能 运行 挑战使用 Angular 和 Jasmine 测试异步代码的人。

另一种处理此问题的方法是在测试承诺时始终使用 .finally(done),然后在之后调用 $timeout.flush()

"use strict";

describe('Services', function() {
  var $q;
  var $timeout;

  // Include your module
  beforeEach(module('plunker'));

  // When Angular is under test it provides altered names
  // for services so that they don't interfere with
  // outer scope variables like we initialized above.
  // This is nice because it allows for you to use $q
  // in your test instead of having to use _q , $q_ or 
  // some other slightly mangled form of the original
  // service names
  beforeEach(inject(function(_$q_, _$timeout_) {
    $q = _$q_;

    // *** Use angular's timeout, not window.setTimeout() ***
    $timeout = _$timeout_;
  }));

  it('should run DataService.isReady', function(done) {

    // Create a Dataservice
    function  DataService() {}

    // Set up the prototype function isReady
    DataService.prototype.isReady = function () {

      // Keep a reference to this for child scopes
      var _this = this;

      // Create a deferred
      var deferred = $q.defer();

      // If we're ready, start a timeout that will eventually resolve
      if (this.ready) {
        $timeout(function () {

          // *** Note, we're not returning anything so we
          // removed 'return' here. Just calling is needed. ***
          deferred.resolve(true);
        }, 1);
      } else {
        // a bunch of other stuff that eventually returns a promise
        deferred.reject(false);
      }

      // Return the promise now, it will be resolved or rejected in the future
      return deferred.promise;
    };

    // Create an instance
    var ds = new DataService();
    ds.ready = true;

    console.log('got here');

    // Call isReady on this instance
    var prom = ds.isReady();
    console.log(prom.then);

    prom.then(function(result) {
     console.log("I work!");
     expect(result).toBe(true);
    },function(err) {
      console.error(err);
    }, function() {
      console.log('progress?');
    })

    // *** IMPORTANT: done must be called after promise is resolved
    .finally(done);
    $timeout.flush(); // Force digest cycle to resolve promises;
  });
});

http://plnkr.co/edit/LFp214GQcm97Kyv8xnLp