Problem Intro
I'm trying to unit test an AngularJS service that wraps the Facebook
JavaScript SDK FB object; however, the test isn't working,
and I haven't been able to figure out why. Also, the service code does
work when I run it in a browser instead of a JasmineJS unit
test, run with Karma test runner.
I'm testing an asynchronous method using Angular promises via the $q
object. I have the tests set up to run asynchronously using the Jasmine
1.3.1 async testing methods, but the waitsFor()
function never returns true (see test code below), it just times-out
after 5 seconds. (Karma doesn't ship with the Jasmine 2.0 async testing API yet).
I think it might be because the then() method of the
promise is never triggered (I've got a console.log() set up to show
that), even though I'm calling $scope.$apply() when the asynchronous
method returns, to let Angular know that it should run a digest
cycle and trigger the then() callback...but I could be wrong.
This is the error output that comes from running the test:
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)
The Code
This is my unit test for the service (see inline comments that explain what I've found so far):
'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
And this is my Angular service that is being tested (see inline comments that explain what I've found so far):
'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
};
}]);
Resources
Here is a list of other resources that I've already taken a look at to
try to solve this problem.
Angular API Reference: $q
This explains how to use promises in Angular, as well as giving an example of how to unit-test code that uses promises (note the explanation of why $scope.$apply() needs to be called to trigger the then() callback).
Jasmine Async Testing Examples
Jasmine.Async: Making Asynchronous Testing With Jasmine Suck Less
Testing Asynchronous Javascript w/ Jasmine 2.0.0
These give examples of how to use the Jasmine 1.3.1 async methods to test objects implementing the Promise pattern. They're slightly different from the pattern I used in my own test, which is modeled after the example that comes directly from the Jasmine 1.3.1 async testing documentation.
StackOverflow Answers
Promise callback not called in Angular JS
Answer 1
Answer 2
angularjs - promise never resolved in controller
AngularJS promises not firing when returned from a service
Summary
Please note that I'm aware that there are already other Angular libraries out there for the Facebook JavaScript SDK, such as the following:
angular-easyfb
angular-facebook
I'm not interested in using them right now, because I wanted to learn how to write an Angular service myself. So please keep answers restricted to helping me fix the problems in my code, instead of suggesting that I use someone else's.
So, with that being said, does anyone know why my test isn't working?
TL;DR
Call $rootScope.$digest() from your test code and it'll pass:
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.
Note: I removed run() and wait() calls because they're not needed here (no actual async calls being performed).
Long explanation
Here's what's happening: When you call getUserLoginStatus(), it internally runs FB.getLoginStatus() which in turn executes its callback immediately, as it should, since you've mocked it to do precisely that. But your $scope.$apply() call is within that callback, so it gets executed before the .then() statement in the test. And since then() creates a new promise, a new digest is required for that promise to get resolved.
I believe this problem doesn't happen in the browser because of one out of two reasons:
FB.getLoginStatus() doesn't invoke its callback immediately so any then() calls run first; or
Something else in the application triggers a new digest cycle.
So, to wrap it up, if you create a promise within a test, explicitly or not, you'll have to trigger a digest cycle at some point in order for that promise to get resolved.
'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
This should do what you're looking for. By using the beforeEach to set up your rootScope and afterEach to run the apply, you're also making your test easily extendable so you can add a test for if the user is logged in.
From what I can see the problem of why your code isnt working is that you havent injected $scope. Michiels answer works cuz he injects $rootScope and call the digest cycle. However your $apply() is a higher level was of invoking the digest cycle so it will work as well... BUT! only if you inject it into the service itself.
But i think a service doesn't create a $scope child so you need to inject $rootScope itself - as far as I know only controllers allow you to inject $scope as its their job to create well a $scope. But this is a bit of speculation, I am not 100 percent sure about it. I would try however with $rootScope as you know the app has a $rootScope from the creation of ng-app.
'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
};
}]);
Related
It seems that promises do not resolve in Angular/Jasmine tests unless you force a $scope.$digest(). This is silly IMO but fine, I have that working where applicable (controllers).
The situation I'm in now is I have a service which could care less about any scopes in the application, all it does it return some data from the server but the promise doesn't seem to be resolving.
app.service('myService', function($q) {
return {
getSomething: function() {
var deferred = $q.defer();
deferred.resolve('test');
return deferred.promise;
}
}
});
describe('Method: getSomething', function() {
// In this case the expect()s are never executed
it('should get something', function(done) {
var promise = myService.getSomething();
promise.then(function(resp) {
expect(resp).toBe('test');
expect(1).toEqual(2);
});
done();
});
// This throws an error because done() is never called.
// Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
it('should get something', function(done) {
var promise = myService.getSomething();
promise.then(function(resp) {
expect(resp).toBe('test');
expect(1).toEqual(2);
done();
});
});
});
What is the correct way to test this functionality?
Edit: Solution for reference. Apparently you are forced to inject and digest the $rootScope even if the service is not using it.
it('should get something', function($rootScope, done) {
var promise = myService.getSomething();
promise.then(function(resp) {
expect(resp).toBe('test');
});
$rootScope.$digest();
done();
});
You need to inject $rootScope in your test and trigger $digest on it.
there is always the $rootScope, use it
inject(function($rootScope){
myRootScope=$rootScope;
})
....
myRootScope.$digest();
So I have be struggling with this all afternoon. After reading this post, I too felt that there was something off with the answer;it turns out there is. None of the above answers give a clear explanation as to where and why to use $rootScope.$digest. So, here is what I came up with.
First off why? You need to use $rootScope.$digest whenever you are responding from a non-angular event or callback. This would include pure DOM events, jQuery events, and other 3rd party Promise libraries other than $q which is part of angular.
Secondly where? In your code, NOT your test. There is no need to inject $rootScope into your test, it is only needed in your actual angular service. That is where all of the above fail to make clear what the answer is, they show $rootScope.$digest as being called from the test.
I hope this helps the next person that comes a long that has is same issue.
Update
I deleted this post yesterday when it got voted down. Today I continued to have this problem trying to use the answers, graciously provided above. So, I standby my answer at the cost of reputation points, and as such , I am undeleting it.
This is what you need in event handlers that are non-angular, and you are using $q and trying to test with Jasmine.
something.on('ready', function(err) {
$rootScope.$apply(function(){deferred.resolve()});
});
Note that it may need to be wrapped in a $timeout in some case.
something.on('ready', function(err) {
$timeout(function(){
$rootScope.$apply(function(){deferred.resolve()});
});
});
One more note. In the original problem examples you are calling done at the wrong time. You need to call done inside of the then method (or the catch or finally), of the promise, after is resolves. You are calling it before the promise resolves, which is causing the it clause to terminate.
From the angular documentation.
https://docs.angularjs.org/api/ng/service/$q
it('should simulate promise', inject(function($q, $rootScope) {
var deferred = $q.defer();
var promise = deferred.promise;
var resolvedValue;
promise.then(function(value) { resolvedValue = value; });
expect(resolvedValue).toBeUndefined();
// Simulate resolving of promise
deferred.resolve(123);
// Note that the 'then' function does not get called synchronously.
// This is because we want the promise API to always be async, whether or not
// it got called synchronously or asynchronously.
expect(resolvedValue).toBeUndefined();
// Propagate promise resolution to 'then' functions using $apply().
$rootScope.$apply();
expect(resolvedValue).toEqual(123);
}));
I have a small Angular service that handles asynchronous rate-limiting of other functions, similar to this example. Since the main purpose of this class is to moderate asynchronous behaviors, I'd like to be able to test this service asynchronously - I won't be able to prove that this class is working with a purely synchronous test.
If I understand correctly, when Angular's ngMock module is loaded, the built-in $timeout service is replaced with a mocked version of $timeout which allows tests to synchronously run functions that are ordinarily asynchronous. However, in this case, I'd like to use the real implementation of $timeout instead of the mocked version.
How can I inject the real implementation of $timeout into my unit test?
Here's what my tests looks like currently (I'm writing my tests in TypeScript):
describe('My Tests', () => {
let myService: MyService,
$timeout: ng.ITimeoutService;
beforeEach(() => {
inject(($injector) => {
// this gets me the mocked version of $timeout
$timeout = $injector.get('$timeout');
});
myService = new MyService($timeout);
jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000;
});
it('should pass', (done) => {
$timeout(50)
.then(() => {
// this is never called
expect(1).toBe(1);
})
.finally(done);
});
});
When I run this test, Karma complains that the test too took long because the mocked$timeout service never actually kicks off its deferred timeout:
Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
You need to call $timeout.flush(); to force all $timeout inside your controller to be released :
it('a ctrl with $timeout inside', inject(function($timeout) {
var myCOntroller = $controller('Controller', { $scope: $scope });
// flush timeout(s) for all code under test.
$timeout.flush();
// this will throw an exception if there are any pending timeouts.
$timeout.verifyNoPendingTasks();
expect($scope.result).toBe("whatIexpect");
}));
// with your example
it('should pass', (done) => {
$timeout(50)
.then(() => {
// this is never called
expect(1).toBe(1);
})
.finally(done);
$timeout.flush();
});
Everything better explain here :)
In addition, you should never use the real $timeout because it will REALLY slowing down your tests...
Updated with HTTP and initial code based on requests/Please look at the bottom of the post:
I've been posting several questions on my AngularJS learning curve of late and the SO community has been fantastic. I've been a traditional C programmer when I used to program and have recently started writing my own ionic/Angular JS app. I'm struggling with the promise version of traditional async calls when it comes to converting a custom function to a promise. I don't think I really understood and I find various examples very contrived. I'd appreciate some help. I have some code which is not working, and I have some conceptual questions:
Let's take this simple function:
angular.module('zmApp.controllers').service('ZMDataModel', function() { return { getMonitors: function () { return monitors; } }
getMonitors is a simple function that basically returns an array of monitors. But here is the rub: When the app first starts, I call an http factory that does an http get and goes about populating this monitor list. This http factory is different from this service but invokes a setMonitor method in this service to populate the array. When the array is populated, a variable called 'monitorsLoaded' is set to 1. When this variable is set to 1, I know for sure monitors is loaded.
Now, I have a view with a controller called "MontageCtrl". I want to wait for the monitors to load before I show the view. In a previous post, one person suggested I use route resolve, but I had to first convert my getMonitors to a promise. So here is what I did:
angular.module('zmApp.controllers').service('ZMDataModel', function($q) {
getMonitors: function () {
var _deferred = $q.defer();
if (monitorsLoaded!=0)
{
console.log ("**** RETURNING MONITORS *****");
_deferred.resolve(monitors);
}
console.log ("*** RETURNING PROMISE ***");
return _deferred.promise;
},
Next up, in app.js I connected the route as follows:
.state('app.montage', {
data: {requireLogin:false},
resolve: {
message: function(ZMDataModel)
{
console.log ("Inside app.montage resolve");
return ZMDataModel.getMonitors();
}
},
Finally I modified my controller to grab the promise as such:
angular.module('zmApp.controllers').controller('zmApp.MontageCtrl', function($scope,$rootScope, ZMHttpFactory, ZMDataModel,message) {
//var monsize =3;
console.log ("********* Inside Montage Ctrl");
It seems based on logs, I never go inside Montage Ctrl. Route resolve seems to be waiting for ever, whereas my logs are showing that after a while, monitorLoaded is being set to 1.
I have several conceptual questions:
a) In function getMonitors, which I crafted as per examples, why do people return a _deferred.promise but only assign a _deferred.resolve? (i.e. why not return it too?). Does it automatically return?
b) I noticed that if I moved var _deferred definition to my service and out of its sub function, it did work, but the next view that had the same route dependency did not. I'm very confused.
c) Finally I ready somewhere that there is a distinction between a service and a factory when it comes to route resolve as a service is only instantiated once. I am also very confused as in some route resolve examples people use when, and I am using .state.
At this stage, I'm deep into my own confusion. Can someone help clarify? All I really want is for various views to wait till monitorsLoaded is 1. And I want to do it via route resolves and promises, so I get the hang of promises once and for all.
Added: Here is the HTTP factory code as well as the app.run code that calls this when the app first starts. FYI, the http factory works well - the problems started when I crafted ZMDataModel - I wanted this to be a central data repository for all controllers to use -- so they did not have to call HTTP Factory each time to access data, and I could control when HTTP factory needs to be called
angular.module('zmApp.controllers').factory('ZMHttpFactory', ['$http', '$rootScope','$ionicLoading', '$ionicPopup','$timeout','ZMDataModel',
function($http, $rootScope, $ionicLoading, $ionicPopup, $timeout,ZMDataModel) {
return {
getMonitors: function() {
var monitors = [];
var apiurl = ZMDataModel.getLogin().apiurl;
var myurl = apiurl+"/monitors.json";
return $http({
url: myurl,
method: 'get'
}) //http
.then(function(response) {
var data = response.data;
//console.log("****YAY" + JSON.stringify(data));
// $rootScope.$broadcast ('handleZoneMinderMonitorsUpdate',monitors);
$ionicLoading.hide();
ZMDataModel.setMonitors(data.monitors);
ZMDataModel.setMonitorsLoaded(1);
//monitors = data.monitors;
return ZMDataModel.getMonitors();
},
function (result)
{
console.log ("**** Error in HTTP");
$ionicLoading.hide();
ZMDataModel.setMonitorsLoaded(1);
//$ionicPopup.alert ({title: "Error", template:"Error retrieving Monitors. \nPlease check if your Settings are correct. "});
return ZMDataModel.getMonitors();
}
); //then
}, //getMonitors
And here is the code in app.run that first calls this:
.run(function($ionicPlatform, $ionicPopup, $rootScope, $state,ZMDataModel, ZMHttpFactory)
{
ZMDataModel.init();
var loginData = ZMDataModel.getLogin();
if ( loginData.username && loginData.password && loginData.url && loginData.apiurl)
{
console.log ("VALID CREDENTIALS. Grabbing Monitors");
// this calls http factory getMonitors that eventually populated the ZMDataModel
// monitors array and sets monitorsLoaded to 1
ZMHttpFactory.getMonitors();
}
}
I finally solved all the problems. There were various issues with my initial attempts. My final resolved solution is here Am I returning this promise correctly?
The learnings:
a) Separating the HTTP get into a factory and the data model into another service was unnecessarily complicating life. But that separation was not the problem. Infact, the way the promise was coded above, on first run, if monitorsLoaded was 0, it would simply return the deferred promise and there was no ".success" or similar construct for me to get into the resolve code block again.
b) The biggest thing that was making me run around in loops was deferring or rejecting was simply setting a state. the return always has to be the promise - and it would return the state you set. I assumed return d.promise always means returning "in progress".
I am learning how to use AngularJS promises and I'm having a problem writing unit tests for them. I wrote a module with a factory that can query for an RSS feed, parse the titles from the articles, and return a promise that will resolve an array of the title strings.
Here is the module code:
angular.module('rss', [])
.factory('rssService', ['$http', '$q', '$rootScope', function($http, $q,
$rootScope) {
return function(url) {
this.getTitles = function() {
var deferred = $q.defer(),
titles = [];
$http({
method: 'GET',
url: url
}).success(function(data) {
$(data).find('title').each(function(index, item) {
var title = $(item).text();
console.log(title);
titles.push(title);
});
deferred.resolve(titles);
});
return deferred.promise;
};
};
}])
;
And here is my Jasmine unit test code:
beforeEach(module('rss'));
describe("RSS Module", function() {
var rssService, $httpBackend;
beforeEach(inject(function(_rssService_, _$httpBackend_) {
rssService = new _rssService_('/rss');
$httpBackend = _$httpBackend_;
}));
it("Can parse an RSS Feed and get the titles", function(done) {
$httpBackend.expectGET('/rss').respond(mockRSS); // mock data declared earlier
var titles = rssService.getTitles();
$httpBackend.flush();
titles.then(function(data) {
console.log("Done!");
expect(data.length).toBe(17);
done();
});
});
});
When I run the unit test, I get the following error:
Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
Which is because, while my promise resolved, it did not make a call to $apply the changes. The solution I've read about was to inject a $rootScope and call $rootScope.$apply() after I resolve the promise. Doing this though results in the following error
Error: [$rootScope:inprog] $digest already in progress
which is because I'm calling $rootScope.$apply() inside of the success() block, so a digest is already happening.
Outside of a unit test (using an actual RSS feed) the code works fine. The controller's scope handles the digest and things work as expected.
Is there any way around this?
Your answer is right about the order of flush and then. The flush should be after the callbacks have been defined.
Also, when you're using flush from $httpBackend, or from $timeout, there is no need to use done: the test becomes synchronous. So the test can be:
it("Can parse an RSS Feed and get the titles", function() {
$httpBackend.expectGET('/rss').respond(mockRSS); // mock data declared earlier
rssService.getTitles().then(function(data) {
expect(data.length).toBe(17);
});
$httpBackend.flush();
});
Also, I don't think directly related to your issue, but there isn't usually a need to manually create a promise when you want to post-process the result of $http. Using the then callback of $http, instead of the custom success, you can use standard promise chaining:
this.getTitles = function() {
return $http({
method: 'GET',
url: url
}).then(function(response) {
var derivedData = {length: response.data.length};
return derivedData;
});
};
You can see this at http://plnkr.co/edit/efTzOU9RYhVIgLVMdKkd .
The solution I found was to move my line of code that flushed the $httpBackend to after the code to declare the then block. What was happening was the GET request was being flushed and my promise would be declared and resolved before I had given it a then.
The new unit test code looks like this:
it("Can parse an RSS Feed and get the titles", function(done) {
$httpBackend.expectGET('/rss').respond(mockRSS);
var titles = rssService.getTitles();
titles.then(function(data) {
console.log("Done!");
expect(data.length).toBe(17);
done();
});
$httpBackend.flush();
});
I didn't have to change any of my actual module code, but I'm currious if there is a better way to write it using chained promises. Comments or additional answers are very welcome.
Folks, I have a problem. I'm using AngularJS and I am setting up a deferred object inside an Angular service definition:
angular.module('myServices', []).
service('Brand', function($rootScope, $q){
var service = {
getNext: function() {
var deferred = $q.defer();
setTimeout(function() {
deferred.resolve('foo');
}, 2000);
return deferred.promise;
}
};
return service;
});
The service is used in my controller:
angular.module({
controllers: {
brand: function($scope, Brand) {
$scope.changeBrand = function() {
$scope.brand = Brand.getNext();
}
}
}
}, ['myServices]);
And finally the view waits for the promise to be resolved and then displays it:
<a ng-click="changeBrand()" id="changeBrand">Change</a>
<p ng-bind="brand"></p>
The trouble is that although the promise is being resolved and although the view does just fine waiting for the promise to be resolved and showing the result, it doesn't do it immediately. It only shows up when I add this code and click the link:
// View:
<a ng-click="apply()">Apply</a>
// Controller:
$scope.apply = function() {
$scope.$apply();
};
Does the part of the digest that the promise lives in exist separately from the digest that runs when the view/controller's scope is changed? How can I get the digest to run on the view/controller's scope automatically when the deferred resolves?
Thanks!
Few days ago I had the same issue while implementing lazy controllers with promisses. When you take a look at the documentation about routeProvider there is a sample that uses $timeout service with promises (https://github.com/angular/angular.js/blob/master/src/ng/route.js#L186). $timeout implemation uses $rootScope.$apply() to call the lifecycle and refresh the bindings (AFAIK it calls all dirty check functions - it's how bindings work in Angular). So the solution to my problem (and I think also yours) was adding $rootScope.$apply() after the resolve(), like this:
setTimeout(function() {
deferred.resolve('foo');
$rootScope.$apply();
}, 2000);