在JasmineJStesting中不会触发AngularJS Promisecallback

问题介绍

我试图unit testing一个包装Facebook JavaScript SDK FB对象的AngularJS服务; 然而,testing不起作用,我还没有弄清楚为什么。 另外,当我在浏览器中运行服务代码,而不是使用Karmatesting运行器运行的JasmineJSunit testing时,服务代码就可以工作。

我正在通过$q对象testing一个使用Angular promise的asynchronous方法。 我设置了使用Jasmine 1.3.1asynchronoustesting方法asynchronous运行的testing ,但waitsFor()函数永远不会返回true (请参阅下面的testing代码),它在5秒后超时 。 (Karma不包含Jasmine 2.0asynchronoustestingAPI)。

我认为这可能是因为承诺的then()方法从来没有触发(我有一个console.log()设置来显示),即使我打电话给$scope.$apply()asynchronous方法返回,让Angular知道它应该运行一个摘要循环,并触发then()callback…但我可能是错的。

这是运行testing的错误输出:

 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) 

代码

这是我对服务的unit testing( 请参阅内联注释,以解释我迄今为止发现的内容 ):

 '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 

这是我正在testing的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 }; }]); 

资源

下面是我已经看过的其他资源的列表,试图解决这个问题。

  • Angular API参考: $q

    这就解释了如何在Angular中使用promise,并举例说明如何对使用promise的代码进行unit testing(请注意为什么需要调用$scope.$apply()来触发then()callback) 。

  • 茉莉花asynchronoustesting的例子

    • Jasmine.Async:使用Jasmine进行asynchronoustesting更less
    • testingasynchronousJavaScript瓦特/茉莉花2.0.0

      这些例子给出了如何使用Jasmine 1.3.1asynchronous方法来testing实现Promise模式的对象。 它们与我在自己的testing中使用的模式略有不同,后者是在Jasmine 1.3.1asynchronoustesting文档直接提供的示例之后build立的。

  • StackOverflow答案

    • 在Angular JS中没有调用Promisecallback
      • 答案1
      • 答案2
    • angularjs – 承诺永远不会在控制器中解决
    • AngularJS承诺从服务返回时不会触发

概要

请注意,我知道在Facebook JavaScript SDK中已经有了其他的Angular库 ,比如:

  • angulareasyfb
  • angular的Facebook

我现在不想使用它们,因为我想学习如何自己编写一个Angular服务。 所以请保持答案限制,以帮助我解决我的代码中的问题,而不是build议我使用别人的

所以,这样说,有没有人知道为什么我的testing不工作?

TL; DR

从testing代码中调用$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()调用,因为这里不需要它们(没有执行实际的asynchronous调用)。

很长的解释

以下是发生了什么事情:当你调用getUserLoginStatus() ,它会在内部运行FB.getLoginStatus() ,它会立即执行它的callback,因为它已经嘲笑它做到了这一点。 但是您的$scope.$apply()调用在该callback中,所以它在testing中的.then()语句之前执行。 自then()创build一个新的承诺,需要一个新的摘要来解决这个承诺。

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

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

所以,要把它包装起来,如果你在一个testing中创build一个承诺,不pipe是否明确,你都必须在某个时刻触发一个摘要循环,以便解决这个承诺。

  '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来运行应用程序,你也使得你的testing很容易扩展,所以你可以添加一个testing,如果用户login。

从我可以看到为什么你的代码不工作的问题是你没有注入$范围。 Michiels回答的工作因为他注入$ rootScope并调用摘要循环。 然而,你的$ apply()是一个更高级别的调用摘要循环,所以它会工作以及…但! 只有当你注入服务本身。

但我认为一个服务不会创build一个$ scope的孩子,所以你需要注入$ rootScope本身 – 据我所知,只有控制器允许你注入$ scope作为他们的工作来创build一个$ scope。 但是这是一个猜测,我不是100%确定的。 我会尝试使用$ rootScope,因为您知道应用程序具有创buildng-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 }; }]); 
Interesting Posts