JasmineJS 测试中未触发 AngularJS Promise 回调

我正在尝试对包装 Facebook 的 AngularJS 服务进行单元测试JavaScript SDK FB 对象;但是,测试不起作用,我一直无法弄清楚为什么.此外,服务代码确实当我在浏览器中运行它而不是 JasmineJS 时工作单元测试,使用 Karma 测试运行器运行.

I'm trying to unit test an AngularJS service that wraps the Facebook JavaScript SDK FB object; however, the test isn't working, and I haven't been able to figure out why. Also, the service code does work when I run it in a browser instead of a JasmineJS unit test, run with Karma test runner.

我正在通过 $q<使用 Angular 承诺测试异步方法/代码>目的.我将测试设置为使用 Jasmine 异步运行1.3.1 异步测试方法,但waitsFor()函数从不返回 true(见下面的测试代码),它只是超时5 秒后.(Karma 尚未附带 Jasmine 2.0 异步测试 API).

I'm testing an asynchronous method using Angular promises via the $q object. I have the tests set up to run asynchronously using the Jasmine 1.3.1 async testing methods, but the waitsFor() function never returns true (see test code below), it just times-out after 5 seconds. (Karma doesn't ship with the Jasmine 2.0 async testing API yet).

我想可能是因为 then() 方法承诺永远不会被触发(我有一个 console.log() 设置显示那),即使我在异步调用 $scope.$apply()方法返回,让 Angular 知道它应该运行一个摘要循环并触发 then() 回调......但我可能是错的.

I think it might be because the then() method of the promise is never triggered (I've got a console.log() set up to show that), even though I'm calling $scope.$apply() when the asynchronous method returns, to let Angular know that it should run a digest cycle and trigger the then() callback...but I could be wrong.

这是运行测试的错误输出:

This is the error output that comes from running the test:

Chrome 32.0.1700 (Mac OS X 10.9.1) service Facebook should return false
  if user is not logged into Facebook FAILED
  timeout: timed out after 5000 msec waiting for something to happen
Chrome 32.0.1700 (Mac OS X 10.9.1):
  Executed 6 of 6 (1 FAILED) (5.722 secs / 5.574 secs)

代码

这是我对服务的单元测试(查看解释我目前发现的内容的内联注释):

'use strict';

describe('service', function () {
  beforeEach(module('app.services'));

  describe('Facebook', function () {
    it('should return false if user is not logged into Facebook', function () {
      // Provide a fake version of the Facebook JavaScript SDK `FB` object:
      module(function ($provide) {
        $provide.value('fbsdk', {
          getLoginStatus: function (callback) { return callback({}); },
          init: function () {}
        });
      });

      var done = false;
      var userLoggedIn = false;

      runs(function () {
        inject(function (Facebook, $rootScope) {
          Facebook.getUserLoginStatus($rootScope)
            // This `then()` callback never runs, even after I call
            // `$scope.$apply()` in the service :(
            .then(function (data) {
              console.log("Found data!");
              userLoggedIn = data;
            })
            .finally(function () {
              console.log("Setting `done`...");
              done = true;
            });
        });
      });

      // This just times-out after 5 seconds because `done` is never
      // updated to `true` in the `then()` method above :(
      waitsFor(function () {
        return done;
      });

      runs(function () {
        expect(userLoggedIn).toEqual(false);
      });

    }); // it()
  }); // Facebook spec
}); // Service module spec

这是我正在测试的 Angular 服务(查看解释我目前发现的内容的内联注释):

And this is my Angular service that is being tested (see inline comments that explain what I've found so far):

'use strict';

angular.module('app.services', [])
  .value('fbsdk', window.FB)
  .factory('Facebook', ['fbsdk', '$q', function (FB, $q) {

    FB.init({
      appId: 'xxxxxxxxxxxxxxx',
      cookie: false,
      status: false,
      xfbml: false
    });

    function getUserLoginStatus ($scope) {
      var deferred = $q.defer();
      // This is where the deferred promise is resolved. Notice that I call
      // `$scope.$apply()` at the end to let Angular know to trigger the
      // `then()` callback in the caller of `getUserLoginStatus()`.
      FB.getLoginStatus(function (response) {
        if (response.authResponse) {
          deferred.resolve(true);
        } else {
          deferred.resolve(false)
        }
        $scope.$apply(); // <-- Tell Angular to trigger `then()`.
      });

      return deferred.promise;
    }

    return {
      getUserLoginStatus: getUserLoginStatus
    };
  }]);

资源

这是我已经查看过的其他资源的列表尝试解决这个问题.

Resources

Here is a list of other resources that I've already taken a look at to try to solve this problem.

  • Angular API 参考:$q

这解释了如何在 Angular 中使用 Promise,并给出了一个如何对使用 Promise 的代码进行单元测试的示例(注意为什么 $scope.$apply() 需要被调用以触发 then() 回调).

This explains how to use promises in Angular, as well as giving an example of how to unit-test code that uses promises (note the explanation of why $scope.$apply() needs to be called to trigger the then() callback).

Jasmine 异步测试示例

Jasmine Async Testing Examples

  • Jasmine.Async:使用 Jasmine 减少异步测试
  • 测试带 Jasmine 2.0.0 的异步 Javascript

这些示例说明了如何使用 Jasmine 1.3.1 异步方法来测试实现 Promise 模式的对象.它们与我在自己的测试中使用的模式略有不同,该模式是根据直接来自 Jasmine 1.3.1 异步测试文档.

These give examples of how to use the Jasmine 1.3.1 async methods to test objects implementing the Promise pattern. They're slightly different from the pattern I used in my own test, which is modeled after the example that comes directly from the Jasmine 1.3.1 async testing documentation.

StackOverflow 答案

StackOverflow Answers

  • 在 Angular JS 中未调用 Promise 回调
    • 答案1
    • 答案2

    请注意,我知道已经有其他适用于 Facebook JavaScript SDK 的 Angular 库,例如:

    • angular-easyfb
    • angular-facebook

    我现在对使用它们不感兴趣,因为我想学习如何自己编写 Angular 服务.因此,请将答案限制在帮助我解决我的代码中的问题,而不是建议我使用其他人的.

    I'm not interested in using them right now, because I wanted to learn how to write an Angular service myself. So please keep answers restricted to helping me fix the problems in my code, instead of suggesting that I use someone else's.

    那么,话虽如此,有人知道为什么我的测试不起作用吗?

    推荐答案

    TL;DR

    从您的测试代码中调用 $rootScope.$digest() ,它会通过:

    Call $rootScope.$digest() from your test code and it'll pass:

    it('should return false if user is not logged into Facebook', function () {
      ...
    
      var userLoggedIn;
    
      inject(function (Facebook, $rootScope) {
        Facebook.getUserLoginStatus($rootScope).then(function (data) {
          console.log("Found data!");
          userLoggedIn = data;
        });
    
        $rootScope.$digest(); // <-- This will resolve the promise created above
        expect(userLoggedIn).toEqual(false);
      });
    });
    

    Plunker这里.

    注意:我删除了 run()wait() 调用,因为这里不需要它们(没有执行实际的异步调用).

    Note: I removed run() and wait() calls because they're not needed here (no actual async calls being performed).

    详细解释

    这是发生了什么:当您调用 getUserLoginStatus() 时,它会在内部运行 FB.getLoginStatus(),而后者又会立即执行其回调,因为它应该这样做,因为你已经嘲笑它来做到这一点.但是您的 $scope.$apply() 调用在该回调中,因此它在测试中的 .then() 语句之前执行.由于 then() 创建了一个新的 Promise,因此该 Promise 需要一个新的摘要才能得到解决.

    Here's what's happening: When you call getUserLoginStatus(), it internally runs FB.getLoginStatus() which in turn executes its callback immediately, as it should, since you've mocked it to do precisely that. But your $scope.$apply() call is within that callback, so it gets executed before the .then() statement in the test. And since then() creates a new promise, a new digest is required for that promise to get resolved.

    我相信这个问题不会发生在浏览器中,原因有二:

    I believe this problem doesn't happen in the browser because of one out of two reasons:

    1. FB.getLoginStatus() 不会立即调用其回调,因此任何 then() 调用都会首先运行;或
    2. 应用程序中的其他内容会触发新的摘要循环.
    1. FB.getLoginStatus() doesn't invoke its callback immediately so any then() calls run first; or
    2. Something else in the application triggers a new digest cycle.

    因此,总结一下,如果您在测试中创建了一个承诺,无论是否明确,您都必须在某个时候触发一个摘要循环才能解决该承诺.

    So, to wrap it up, if you create a promise within a test, explicitly or not, you'll have to trigger a digest cycle at some point in order for that promise to get resolved.

相关文章