【问题标题】:AngularJS Promise Callback Not Trigged in JasmineJS TestJasmine JS 测试中未触发 AngularJS Promise 回调
【发布时间】:2014-02-07 00:06:59
【问题描述】:

问题介绍

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

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

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

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

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)

代码

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

'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 服务(查看内联 cmets 来解释我目前的发现):

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

资源

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

总结

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

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

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

【问题讨论】:

  • 您是否尝试过 $rootScope.$apply 而不是仅在 $scope 上?
  • @hassassin 我的测试实际上是将$rootScope 传递给我的服务,所以服务基本上是在调用$rootScope.$apply()。所以是的,我已经尝试过了。不过谢谢你的建议!
  • 要使用 Jasmine 2.0,您应该能够使用 npm install karma-jasmine@2_0 而不是 npm install karma-jasmine 安装 karma-jasmine

标签: javascript angularjs jasmine karma-runner


【解决方案1】:

TL;DR

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

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

Plunkerhere.

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

详细解释

这是发生的事情:当您调用 getUserLoginStatus() 时,它在内部运行 FB.getLoginStatus(),而 FB.getLoginStatus() 又会立即执行它的回调,因为您已经模拟了它来精确地执行此操作。但是您的$scope.$apply() 调用在该回调中,因此它在测试中的.then() 语句之前执行。由于then() 创建了一个新的承诺,因此需要一个新的摘要来解决该承诺。

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

  1. FB.getLoginStatus() 不会立即调用其回调,因此任何 then() 调用都会先运行;或
  2. 应用程序中的其他内容会触发新的摘要循环。

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

【讨论】:

  • 我还是不明白,但它有效。谢谢。
  • 我已经为一个相关的错误苦苦思索了三天,而你刚刚解决了它。谢谢。
  • 我遇到了同样的问题,我已经尝试了 $rootScope.$apply 和 $rootScope.$digest 并且它也没有调用我的 .then() 。还有其他想法吗?在我的情况下,它甚至没有调用在 $http() 调用之后直接进行的 .then() 调用,我从测试中返回了 json 响应。
【解决方案2】:
    'use strict';

describe('service: Facebook', function () {
    var rootScope, fb;
    beforeEach(module('app.services'));
    // Inject $rootScope here...
    beforeEach(inject(function($rootScope, Facebook){
        rootScope = $rootScope;
        fb = Facebook;
    }));

    // And run your apply here
    afterEach(function(){
        rootScope.$apply();
    });

    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 () {}
            });
        });
        fb.getUserLoginStatus($rootScope).then(function (data) {
            console.log("Found data!");
            expect(data).toBeFalsy(); // user is not logged in
        });
    });
}); // Service module spec

这应该可以满足您的需求。通过使用 beforeEach 设置您的 rootScope 并使用 afterEach 运行应用程序,您还可以轻松扩展您的测试,以便您可以添加测试以判断用户是否登录。

【讨论】:

  • 稍微按摩一下,您的上述解决方案确实有效。但是,我接受了Michael's answer,因为他还解释了为什么我的测试一开始就不起作用。不过也谢谢你的回答!
【解决方案3】:

据我所知,为什么您的代码无法正常工作的问题是您没有注入 $scope. Michiels 的回答很有效,因为他注入了 $rootScope 并调用了摘要循环。但是,您的 $apply() 是调用摘要循环的更高级别,因此它也可以正常工作...但是!仅当您将其注入服务本身时。

但我认为服务不会创建 $scope 子级,因此您需要自己注入 $rootScope - 据我所知,只有控制器允许您注入 $scope 作为其创建 $scope 的工作。但这只是一种猜测,我不能百分百确定。但是,我会尝试使用 $rootScope,因为您知道该应用在创建 ng-app 时具有 $rootScope。

'use strict';

angular.module('app.services', [])
  .value('fbsdk', window.FB)
  .factory('Facebook', ['fbsdk', '$q', '$rootScope' function (FB, $q, $rootScope) { //<---No $rootScope injection

    //If you want to use a child scope instead then --> var $scope = $rootScope.$new();
    // Otherwise just use $rootScope

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

    function getUserLoginStatus ($scope) { //<--- Use of scope here, but use $rootScope instead
      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()`. USE $rootScope instead!
      });

      return deferred.promise;
    }

    return {
      getUserLoginStatus: getUserLoginStatus
    };
  }]);

【讨论】:

    猜你喜欢
    • 2014-06-19
    • 1970-01-01
    • 1970-01-01
    • 2019-04-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多