繁体   English   中英

在JasmineJS测试中未触发AngularJS Promise回调

[英]AngularJS Promise Callback Not Trigged in JasmineJS Test

问题介绍

我正在尝试对包装Facebook JavaScript SDK FB对象的AngularJS服务进行单元测试; 但是,测试不起作用,我无法弄清楚原因。 此外,当我在浏览器中运行它而不是使用Karma测试运行器运行的JasmineJS单元测试时,服务代码可以正常工作。

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

我想这可能是因为promise的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)

代码

这是我对服务的单元测试( 请参阅内联注释,解释到目前为止我发现的内容 ):

'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服务( 请参阅内联注释,解释我到目前为止所发现的内容 ):

'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服务。 所以请保持答案仅限于帮助我解决代码中的问题,而不是建议我使用别人的问题

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

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

这里的人物。

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

很长的解释

这是正在发生的事情:当你调用getUserLoginStatus() ,它会在内部运行FB.getLoginStatus() ,它会立即执行它的回调,因为你已经嘲笑它正是这样做的。 但是$scope.$apply()调用是在该回调中,因此它在测试中的.then()语句之前执行。 从那时then()创建了一个新的承诺,需要一个新的摘要来解决该承诺。

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

  1. FB.getLoginStatus()不会立即调用它的回调,所以任何then()调用都会先运行; 要么
  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来运行apply,您还可以轻松扩展测试,以便在用户登录时添加测试。

从我可以看到你的代码无法工作的问题是你没有注入$ 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
    };
  }]);

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM