我正在尝试对包裹Facebook
JavaScript SDK FB
对象的AngularJS服务进行单元测试;但是,测试不起作用,
我无法弄清楚原因。此外,服务代码也可以
我在浏览器而不是JasmineJS单元中运行时工作
测试,使用Karma test runner运行。
我正在通过$q
使用Angular promises测试异步方法
宾语。我将测试设置为使用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)
这是我对该服务的单元测试(查看解释我到目前为止发现的内联评论):
'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
};
}]);
以下列出了我已经查看过的其他资源 试着解决这个问题。
这解释了如何在Angular中使用promises,以及如何对使用promises的代码进行单元测试的示例(请注意为什么$scope.$apply()
需要被调用以触发{{1}的解释回调)。
Jasmine异步测试示例
Testing Asynchronous Javascript w/ Jasmine 2.0.0
这些示例说明了如何使用Jasmine 1.3.1异步方法来测试实现Promise模式的对象。它们与我在我自己的测试中使用的模式略有不同,模型是在直接来自Jasmine 1.3.1 async testing documentation的示例之后建模的。
StackOverflow答案
请注意,我知道已经有其他用于Facebook JavaScript SDK的Angular库,例如:
我现在对使用它们不感兴趣,因为我想学习如何自己编写Angular服务。 所以请保持答案仅限于帮助我解决我的代码中的问题,而不是建议我使用别人的。
所以,话虽如此,有谁知道为什么我的测试不起作用?
答案 0 :(得分:52)
<强> 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);
});
});
Plunker here。
注意:我删除了run()
和wait()
来电,因为这里不需要它们(没有执行实际的异步调用)。
长解释
以下是正在发生的事情:当您致电getUserLoginStatus()
时,它会在内部运行FB.getLoginStatus()
,而$scope.$apply()
会立即执行其回调,因为您应该将其模拟为做到这一点。但是您的.then()
调用是在该回调中,因此在测试中的then()
语句之前执行。由于FB.getLoginStatus()
创建了新的承诺,因此该承诺需要新的摘要才能得到解决。
我认为这个问题不会在浏览器中发生,因为有两个原因:
then()
不会立即调用其回调,因此任何{{1}}调用都会先运行;或因此,要将其包装起来,如果您在测试中明确或不明确地创建承诺,则必须在某个时刻触发摘要周期,以便该承诺得到解决。
答案 1 :(得分: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,您还可以轻松扩展测试,以便在用户登录时添加测试。
答案 2 :(得分:0)
从我可以看到你的代码无法正常工作的问题是你没有注入$ 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
};
}]);