AngularJS承诺回调在JasmineJS测试中未触发

41

问题介绍

我正在尝试对一个包装了Facebook JavaScript SDK FB对象的AngularJS服务进行单元测试,但是测试不起作用,我也无法弄清原因。同时,在使用Karma测试运行器运行JasmineJS单元测试时,服务代码在浏览器中运行时是有效的。

我正在使用Angular承诺通过$q对象测试异步方法。我已经设置了测试以使用Jasmine 1.3.1异步测试方法运行,但是waitsFor()函数从不返回true(请参见下面的测试代码),它只会在5秒后超时。(Karma还没有使用Jasmine 2.0异步测试API)。
我认为这可能是因为承诺的then()方法从未被触发(我已经设置了console.log()来显示),即使我在异步方法返回时调用$scope.$apply(),让Angular知道它应该运行一个digest循环并触发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服务。 因此,请限制答案以帮助我修复我的代码中的问题,而不是建议我使用别人的代码。

那么,说了这么多,有人知道为什么我的测试没通过吗?


你尝试过使用$rootScope.$apply而不仅仅是$scope吗? - hassassin
@hassassin 我的测试实际上将$rootScope传递给了我的服务,因此服务基本上调用了$rootScope.$apply()。所以,是的,我尝试过了。但是谢谢您的建议! - user456814
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - user276648
3个回答

52

简而言之

在您的测试代码中调用$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);
  });
});

这里是Plunker 链接

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

长说明

这里发生了什么:当你调用getUserLoginStatus()时,它内部运行FB.getLoginStatus(),后者立即执行它的回调函数,因为你已经mocked它做了这件事。但是你的$scope.$apply()调用在该回调函数之内,所以它在测试中的.then()语句之前被执行。由于then()会创建一个新的promise,所以需要一个新的digest来解决该promise。

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

  1. FB.getLoginStatus()不会立即调用它的回调函数,因此任何then()调用都会先执行;或者
  2. 应用程序中的其他内容会触发新的digest周期。

因此,总的来说,如果你在测试中创建了一个promise,无论是显式还是隐式的,你都需要在某个时候触发一个digest周期,以便该promise能够被解决。


3
我仍然不理解它,但它有效。谢谢。 - Nick Perkins
1
我已经为了一个相关的bug苦恼了三天,而你轻松就解决了它。非常感谢你。 - Chris Vandevelde
我也遇到了同样的问题,我尝试了$rootScope.$apply和$rootScope.$digest,但它们都没有调用我的.then()。还有其他想法吗?在我的情况下,它甚至没有调用我在$ http()调用之后直接制作的.then(),而我已经从我的测试中发送了一个json响应。 - robert.bo.roth

5
    '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,您还可以使测试容易扩展,以便添加一个测试,以检查用户是否已登录。


经过一点调整,您上面的解决方案确实可行。然而,我已经接受了Michael的答案,因为他也解释了为什么我的测试一开始没有起作用。但还是谢谢您的回答! - user456814

0

从我所看到的,你的代码不起作用的问题是因为你没有注入 $scope。Michiels 的答案有效是因为他注入了 $rootScope 并调用了 digest 循环。然而,你的 $apply() 是一种更高级别的调用 digest 循环的方式,所以它也可以工作... 但是!只有在将其注入到服务本身中时才会起作用。

但我认为服务不会创建 $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
    };
  }]);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接