AngularJS Promise Callback не запускается в JasmineJS Test
Ввод проблемы
Я пытаюсь использовать unit test службу AngularJS, которая обертывает Facebook
JavaScript SDK FB
объект; однако тест не работает,
и я не смог понять, почему. Кроме того, код службы
когда я запускаю его в браузере вместо JasmineJS
тест, выполните Тест-драйв кармы.
Я тестирую асинхронный метод, используя Angular promises через $q
объект. У меня есть тесты, настроенные для асинхронного запуска, используя Жасмин
1.3.1 методы асинхронного тестирования, но waitsFor()
функция никогда не возвращает true
(см. тестовый код ниже), это просто тайм-аут
через 5 секунд. (Карма еще не поставляется с API асинхронного тестирования Jasmine 2.0).
Я думаю, это может быть потому, что метод 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)
Код
Это мой unit test для службы (см. встроенные комментарии, которые объясняют, что я нашел до сих пор):
'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 Ссылка на API: $q
Это объясняет, как использовать promises в Angular, а также пример использования модульного теста, который использует promises (обратите внимание на объяснение причин, почему $scope.$apply()
необходимо вызвать для запуска then()
).
-
Примеры тестирования асинхронного жасмина
-
Ответы StackOverflow
Резюме
Обратите внимание, что я знаю, что есть уже другие библиотеки Angular для SDK для JavaScript JavaScript, например:
Мне сейчас не интересно использовать их, потому что я хотел научиться писать службу Angular. Поэтому, пожалуйста, держите ответы ограниченными, помогая мне исправить проблемы в моем коде, вместо того, чтобы предлагать, чтобы я использовал чужую.
Итак, с учетом сказанного, знает кто, почему мой тест не работает?
Ответы
Ответ 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);
});
});
Plunker здесь.
Примечание. Я удалил вызовы run()
и wait()
, потому что они здесь не нужны (никаких текущих асинхронных вызовов не выполняется).
Длительное объяснение
Здесь что происходит: когда вы вызываете getUserLoginStatus()
, он внутренне запускает FB.getLoginStatus()
, который, в свою очередь, выполняет свой обратный вызов немедленно, как и следовало ожидать, поскольку вы издевались над ним именно так. Но ваш вызов $scope.$apply()
находится внутри этого обратного вызова, поэтому он запускается перед оператором .then()
в тесте. И поскольку then()
создает новое обещание, для этого обещания требуется новый дайджест. Чтобы решить проблему, выполните следующие действия.
Я считаю, что эта проблема не возникает в браузере из-за одной из двух причин:
-
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 для запуска приложения, вы также легко расширяете свой тест, чтобы вы могли добавить тест, если пользователь вошел в систему.
Ответ 3
Из того, что я вижу, проблема в том, почему ваш код не работает, заключается в том, что вы не указали $scope. Michiels отвечает, потому что он вводит $rootScope и вызывает цикл дайджеста. Однако ваш $apply() - это более высокий уровень, вызывающий цикл дайджеста, поэтому он будет работать также... НО! только если вы введете его в саму службу.
Но я думаю, что служба не создает дочерний объект $scope, поэтому вам нужно вводить сам $rootScope - насколько я знаю, только контроллеры позволяют вам вводить $scope в качестве своей работы, чтобы хорошо создать область $scope. Но это немного спекуляция, я не уверен на 100 процентов. Я бы попробовал, однако, с $rootScope, поскольку вы знаете, что приложение имеет $rootScope от создания ng-приложения.
'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
};
}]);