JS mock promise on http call [duplicate] - javascript

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);
}));

Related

httpBackend Mock AJAX ES6 Promise in $q.when

I'm trying to mock a response to a JSONP GET request which is made with a function that returns an ES6 promise which I've wrapped in $q.when(). The code itself works just fine, however, in the unit tests the request is not being caught by $httpBackend and goes through right to the actual URL. Thus when flush() is called I get an error stating Error: No pending request to flush !. The JSONP request is made via jQuery's $.getJSON() inside the ES6 promise so I opted to try and catch all outgoing requests by providing a regex instead of a hard-coded URL.
I've been searching all over trying to figure this out for a while now and still have yet to understand what's causing the call to go through. I feel as if the HTTP request in the ES6 promise is being made "outside of Angular" so $httpBackend doesn't know about it / isn't able to catch it, although that may not be the case if the call was being made inside of a $q promise from the get-go. Can anyone possibly tell me why this call is going through and why a simple timeout will work just fine? I've tried all combinations of $scope.$apply, $scope.$digest, and $httpBackend.flush() here, but to no avail.
Maybe some code will explain it better...
Controller
function homeController() {
...
var self = this;
self.getData = function getData() {
$q.when(user.getUserInformation()).then(function() {
self.username = user.username;
});
};
}
Unit Test
...
beforeEach(module('home'));
describe('Controller', function() {
var $httpBackend, scope, ctrl;
beforeEach(inject(function(_$httpBackend_, $rootScope, $componentController) {
$httpBackend = _$httpBackend_;
scope = $rootScope.$new(); // used to try and call $digest or $apply
// have also tried whenGET, when('GET', ..), etc...
$httpBackend.whenJSONP(/.*/)
.respond([
{
"user_information": {
"username": "TestUser",
}
}
]);
ctrl = $componentController("home");
}));
it("should add the username to the controller", function() {
ctrl.getData(); // make HTTP request
$httpBackend.flush(); // Error: No pending request to flush !
expect(ctrl.username).toBe("TestUser");
});
});
...
For some reason this works, however:
it("should add the username to the controller", function() {
ctrl.getData(); // make HTTP request
setTimeout(() => {
// don't even need to call flush, $digest, or $apply...?
expect(ctrl.username).toBe("TestUser");
});
});
Thanks to Graham's comment, I was brought further down a different rabbit hole due to my lack of understanding several things which I will summarize here in case someone ends up in the same situation...
I didn't fully understand how JSONP works. It doesn't rely on XmlHttpRequest at all (see here). Rather than trying to fiddle with mocking responses to these requests through JSONP I simply switched the "debug" flag on the code I was using which disabled JSONP so the calls were then being made via XHR objects (this would fail the same origin policy if real responses were needed from this external API).
Instead of trying to use jasmine-ajax, I simply set a spy on jQuery's getJSON and returned a mock response. This finally sent the mocked response to the ES6 promise, but for some reason the then function of the $q promise object which resulted from wrapping the ES6 promise wasn't being called (nor any other error-handling functions, even finally). I also tried calling $scope.$apply() pretty much anywhere in the off chance it would help, but to no avail.
Basic implementation (in unit test):
...
spyOn($, 'getJSON').and.callFake(function (url, success) {
success({"username": "TestUser"}); // send mock data
});
ctrl.getData(); // make GET request
...
Problem (in controller's source):
// user.getUserInformation() returns an ES6 promise
$q.when(user.getUserInformation()).then(function() {
// this was never being called / reached! (in the unit tests)
});
Ultimately I used #2's implementation to send the data and just wrapped the assertions in the unit test inside of a timeout with no time duration specified. I realize that's not optimal and hopefully isn't how it should be done, but after trying for many hours I've about reached my limit and given up. If anyone has any idea as to how to improve upon this, or why then isn't being called, I would honestly love to hear it.
Unit Test:
...
ctrl.getData(); // make GET request
setTimeout(() => {
expect(ctrl.username).toBe("TestUser"); // works!
});

JavaScript nested promise scoping and AngularJS

I have a real problem with JavaScript promises that I've been trying to solve for the last few hours and I just can't seem to fix it. My experience with promises is limited so I'm open to the idea that my approach is simply incorrect.
Right now I'm building an app that requires a two-step process:
Connect to an external PaaS service, which returns a promise
Within that promise, retrieve some data
Here's a sample of a factory I created:
app.factory('serviceFactory', [
function() {
var getData = function getData() {
service.connect(apiKey).then(function() {
service.getData('dataStore').then(function(result) {
// Retrieve data
return result;
}, errorFunction);
},
errorFunction);
};
return {
getData: getData
};
}
]);
As you can see, there are nested promises here. What's causing me problems is when I try to use the data from the most deeply-nested promise within an AngularJS view. Specifically, I want to use the data from that promise in an ng-repeat statement. But no matter what I try, it just won't show up. I've attempted to assign data within the promise instead of returning, like so:
service.getData('dataStore').then(function(result) {
// Retrieve data
// Assigned the enclosing scope's this to 'self'
self.data = result;
}, errorFunction);
That doesn't work either. I've tried a variety of other approaches, but I just can't seem to get that data to the view. There's no problem getting it to show up in a console.log(data)call, so I know the data is coming back correctly. Does anyone have experience solving a problem like this?
I would suggest that you'll try to avoid nested promises. You can take a look at this blog post, which will let you see how you can avoid 'promise soup' and have promise chaining instead.
As for your question, I would recommend the following:
A quick solution will be to fix your problem. You are returning the factory method wrong:
app.factory('serviceFactory', [
function() {
var getData = function getData() {
return service.connect(apiKey).then(function() {
service.getData('dataStore').then(function(result) {
// Retrieve data
return result;
}, errorFunction);
},
errorFunction);
};//here you should close the 'getData method
return {
getData: getData
};
}
]);
But, you can refactor your code to chain your promises. Something like:
app.factory('serviceFactory', [
function() {
var connect = function connect() {
return service.connect(apiKey);
};
var getData = function getData(data) {
return service.getData(data);
};
return {
getData: getData,
connect: connect
};
}
]);
Now, you can do something like this:
serviceFactory.connect(apiKey)
.then(serviceFactory.getData)
.then(function(result){
//use data here
})
All of this should be tested - you can add a plunker or jsbin if you want a working solution...
EDIT
I think that you have another problem here. You are mixing between serviceFactory and service. I'm not sure that I understand if this is the same service, or which is who. Can you provide a more detailed code or add plunker/jsbin etc.
I've edited this answer, which I originally deleted because I didn't explain what I meant very clearly and it garnered some downvotes (without explanation, but that's my guess). Anyway, here is a more complete answer.
I suspect that your problem is that whatever PaaS you are using has no awareness of Angular, and Angular likewise has no awareness of the PaaS. You say in your question that the PaaS has methods that return promises, but if Angular is not aware of those promises, then, when the promises resolve, Angular does not know to update the DOM. Angular does this via the digest cycle which is where Angular checks everything that it is watching to see if it has changed. When using $q (or other Angular services like $http), Angular knows to automatically kick off a digest cycle when they resolve. It does not, however, kick off a digest cycle when promises created by other means resolve.
This is what I think is happening in your code. Your PaaS is giving you promises, which are resolving properly (you said you can see the results via console), but your HTML is not being updated.
I modified the plunkr we were working on to demonstrate this in action. I created a mock PaaS (not knowing what you are using) that creates promises using jQuery and resolves them. As you can see, when the promises resolve, the result is logged to the console, but the DOM is not resolved.
angular.module("app",[])
.value("mockPaaS", mockPaaS)
.factory("dataFactory", function($q, mockPaaS){
function getData(){
return mockPaaS.connect()
.then(mockPaaS.getData);
}
return {
getData: getData
}
})
.controller("DataController", function (dataFactory) {
var vm = this;
dataFactory.getData().then(function(result){
console.log(result);
vm.dataArr = result;
});
})
.directive("myApp", function(){
return {
bindToController: true,
controller: "DataController",
controllerAs: 'myApp',
template: "<div ng-repeat='i in myApp.dataArr'>{{i}}</div>"
};
});
I was originally suggesting that you could solve this problem by adding a $scope.$apply() after you capture the result of the promise. I've forked the Plunker and you can see here it does, in fact update the DOM.
.controller("DataController", function ($scope, dataFactory) {
var vm = this;
dataFactory.getData().then(function(result){
console.log(result);
vm.dataArr = result;
$scope.$apply();
});
})
There is, however, a more idiomatic solution. When you get a promise from outside angular that you need to use in Angular, you can wrap that promise using $q.when (an Angular aware promise), and when the external promise resolves, Angular should kick off it's digest cycle naturally.
.factory("dataFactory", function($q, mockPaaS){
function getData(){
return $q.when(mockPaaS.connect()
.then(mockPaaS.getData));
}
return {
getData: getData
}
})
.controller("DataController", function (dataFactory) {
var vm = this;
dataFactory.getData().then(function(result){
console.log(result);
vm.dataArr = result;
});
})
Ben Nadel gives a nice explanation of this issue here.

Mocha: assertions within promise not working

So, I'm completely new to mocha, and promises and I was asked to write integration tests, which posts form data to a page, gets an authentication key, and then run tests on the key.
I'm getting the key back properly, which is confirmed by my log statement, but when I run mocha, my test says 0 passing. It doesn't even print out the descriptions for the 'describe' and 'it' statements, implying that they do not work within the promise. Is there away that I can this work, while still giving the assertions access to my authorization key?
require('should');
require('sails');
var Promise = require("promise");
var request = require('request');
describe('Domain Model', function(){
var options = { url:'link',
form:{
username: 'xxxxx#xxxxxx',
password: '0000'
},
timeout: 2000
};
//request auth key
var promise = new Promise(function(resolve,reject){
request.post(options, function(error, response, body){
//body contains the auth key, within a string of JSON
resolve(body);
});
});
//this is where I'm getting lost
promise.then(function(data,test){
var token = (JSON.parse(data))["access_token"];
console.log(token);
describe('Initialize',function(){
it('should be an string', function(){
(token).should.be.type('string');
});
});
});
});
Mocha is not able to find your tests because you're not structuring them correctly. The it() blocks should go immediately within the describe() blocks (and the describe() blocks may be nested within each other as you see fit). If you want to use promises, the promises should go inside the it() blocks, not the other way around. So, as a first pass, the updated code should look something like this (I'm cutting some parts out for brevity):
describe('Domain Model', function(){
var options = { ... };
describe('Initialize',function(){
it('should be an string', function(){
//request auth key
var promise = new Promise(function(resolve,reject){
// ...
});
promise.then(function(data,test){
var token = (JSON.parse(data))["access_token"];
console.log(token);
(token).should.be.type('string');
});
});
});
});
Note that the promise is now contained inside the body of the it(). Mocha should now be able to find your test. However, it won't pass. Why not?
As you probably know, promises are asynchronous. Testing asynchronous code using mocha requires using the done callback - see the guide for more info. Basically, this means the it() block should accept a parameter named done (the name is important!). This done thingy is something that mocha passes in automatically - its presence indicates to mocha that this test contains asynchronous code, and that therefore it has not completed executing until you say so. The way you indicate that your test is done is by executing done, because done is in fact a callback function. You should execute done() at whatever point your test is deemed complete - i.e., at the end of whatever code block is supposed to run last, which in your case is the bottom of the .then() handler for the promise. So, improving upon our last version, the code now looks like this (again, cutting some parts for brevity):
describe('Domain Model', function(){
var options = { ... };
describe('Initialize',function(){
it('should be an string', function(done){
//request auth key
var promise = new Promise(function(resolve,reject){
// ...
});
promise.then(function(data,test){
var token = (JSON.parse(data))["access_token"];
console.log(token);
(token).should.be.type('string');
done();
});
});
});
});
Note that I just added the done parameter to it(), and then I called it at the bottom of the then().
At this point, the code should work, I think... I'm not sure as I'm not able to test it. However, there are a couple more things we could do to improve upon this further.
First, I question your use of promises here. If you had an API for obtaining the access token, then I would opt to have that API return a promise because promises are very convenient for the caller. However, as I'm sure you've noticed, constructing promises can be a bit tedious, and I don't think it adds much value for your code. I would opt to just do this:
describe('Domain Model', function(){
var options = { ... };
describe('Initialize',function(){
it('should be an string', function(done){
//request auth key
request.post(options, function(error, response, body){
//body contains the auth key, within a string of JSON
var token = (JSON.parse(body))["access_token"];
console.log(token);
(token).should.be.type('string');
done();
});
});
});
});
Isn't that so much shorter and sweeter? The code is still asynchronous, so you should still make sure your it() block accepts a done callback, and you should call it when your test has finished.
Now, if you still insist on using promises, then there's one more thing I have to warn you about. What happens if there's an error inside of your .then() handler code? Well, according to the documentation:
If the handler that is called throws an exception then the promise
returned by .then is rejected with that exception.
Do you have a rejection handler for your promise? No. So what does that mean? That means the error will get swallowed up silently. Your test will fail with Error: timeout of 2000ms exceeded, which is because the done handler never got called, but the actual cause of the error won't be shown, and you'll be pulling your hair out trying to figure out what went wrong.
So what can you do? You could use the second parameter to .then() to specify a rejection handler, and in there, you could take advantage of the fact that the done callback that mocha passes in to your test accepts an error argument, so if you call done("something"), your test will fail (which is what we want in this case), and the "something" will be the reason. So here's what that will look like in your case:
describe('Domain Model', function(){
var options = { ... };
describe('Initialize',function(){
it('should be an string', function(done){
//request auth key
var promise = new Promise(function(resolve,reject){
// ...
});
promise.then(function(data){
var token = (JSON.parse(data))["access_token"];
console.log(token);
(token).should.be.type('string');
done();
}, function (err) {
done(err);
});
});
});
});
However, we can do even better. Consider what happens if an error gets thrown from within the rejection handler. Not likely, since you're not doing a whole lot in there - just calling done(err). But what if it does happen? Well, pretty much the same thing - the error will get silently swallowed up, the test will fail with a non-specific timeout error, and you'll be pulling out your hair again. Is there a way we can get that error to bubble up and get rethrown?
As a matter of fact, there is: Both Q and the promise library that you're using have an alternate handler called .done() (not to be confused with mocha's done callback). It's similar to .then(), but with a slightly different behavior around uncaught exceptions. From the documentation:
Promise#done(onFulfilled, onRejected)
The same semantics as .then except that it does not return a promise
and any exceptions are re-thrown so that they can be logged (crashing
the application in non-browser environments)
Perfect, that's exactly what we want. Just be sure to understand when you should use .then(), vs when you should use .done(). The Q API does a fantastic job of explaining (and the promise library you're using has similar behavior - I tested):
The Golden Rule of done vs. then usage is: either return your promise
to someone else, or if the chain ends with you, call done to terminate
it. Terminating with catch is not sufficient because the catch handler
may itself throw an error.
(Note: .catch() appears to be Q-specific, but it's pretty similar to the onRejected callback, which is the second parameter to .then().).
So with that in mind, just replace your last .then() with a .done(). When you use .done(), you can just omit the rejection handler and rely on the promise library to re-throw any unhandled expections, so you will get an error description and a stack trace. With the above in mind, your code now looks like:
describe('Domain Model', function(){
var options = { ... };
describe('Initialize',function(){
it('should be an string', function(done){
//request auth key
var promise = new Promise(function(resolve,reject){
// ...
});
promise.done(function(data){
var token = (JSON.parse(data))["access_token"];
console.log(token);
(token).should.be.type('string');
done();
});
});
});
});
Basically, ignoring the previous code sample, the only difference from the one before that is we're using .done() instead of .then().
Hopefully, this covers most of what you need to get started. There are other things you might want to consider, like potentially retrieving the auth key inside of a before() hook instead of an it() block (because I'm assuming the real thing you're testing is not the retrieval of the key - that's just a prerequisite to test the things you actually want to test, so a hook might be more appropriate - see here). I also question whether or not you should be connecting to an external system from within your tests at all, versus just stubbing it out (that depends on whether these are unit tests or integration tests). And I'm sure you can come up with a better assertion that just checking that token is a string, like using a regex to make sure it matches a pattern, or actually testing a request for a protected resource and making sure that it goes through. But I'll leave those questions for you to think about.

Angular.js promise not resolving when unit testing service with karma

I am trying to unit test an Angular.js service, and need to set an expect on a promise returned from a Mock service (using Jasmine). I am using the karma unit testing framework. The relevant code snippet is below:
// I can't figure out how to do the equivalent of a $scope.$digest here.
var loginStatusPromise = FacebookService.getFacebookToken();
loginStatusPromise.then(function(token) {
expect(false).toBeTruthy(); // If this test passes, there is something going wrong!
expect(token).not.toBeNull(); // The token should be ValidToken
expect(token).toBe('ValidToken');
});
The complete unit test code can be seen here.
The problem is the promise.then statement never fires when karma is executing. Hence, none of my expect statements are executed.
In my controller tests, I use $scope.$digest() to resolve the promises, but I am not clear on how to do this in a service test. As I thought there was no notion of 'scope' in a service test.
Do I have the wrong end of the stick here? Do I need to injecct $rootScope into my service test and then use $digest? Or, is there another way?
I had this problem and resolved it by simply putting a
$rootScope.$apply() at the end of my test
Your FacebookService might be the issue, as suggested by #mpm. Are you sure it doesn't have any http calls happening inside of that Facebook dependency which wouldn't be occurring during unit testing? Are you certain that resolve has been called on the deferred yet?
Assuming that you are using ngFacebook/ngModule a quick note before the solution/ideas is that this project does not have unit tests ! Are you sure you want to use this project ?
I did a quick scan of your Unit Tests on Github and found following missing:-
1) Module initialization.
ngFacebook needs that or you need to initialize your module that does the same thing.
beforeEach(module('ngFacebook'));
OR
beforeEach(module('yieldtome'));
2) Seriously consider mocking ngFacebook module
At unit level tests you are testing your code within a mocked bubble where outside interfaces are stubbed out.
Otherwise) Try adding calling the API as below:-
$rootScope.$apply(function() {
this.FacebookService.getFacebookToken().then(function(){
//your expect code here
});
});
$httpBackend.flush();//mock any anticipated outgoing requests as per [$httpBackend][2]
beforeEach(function(){
var self=this;
inject(function($rootScope,Facebook){
self.$rootScope=$rootScope;
self.Facebook=Facebook;
});
})
it('resolves unless sourcecode broken',function(done){
// I can't figure out how to do the equivalent of a $scope.$digest here.
var loginStatusPromise = this.FacebookService.getFacebookToken();
loginStatusPromise.then(function(token) {
expect(token).toBe('ValidToken');
done();
});
$rootscope.$apply();
});
https://docs.angularjs.org/api/ng/service/$q
I agree with the above answers that a service should have nothing to do with $rootScope.
In my case had a $q promise, that used a second service internally resolving to a promise as well. No way to resolve the external one, unless I added $rootScope.$digest() into my service code (not the test)...
I ended-up writing this quick shim for $q to use in my tests, but be careful, as it's just an example and not a complete $q implementation.
beforeEach(module(function($provide) {
$provide.value('$q', {
defer: function() {
var _resolve, _reject;
return {
promise: {
then: function (resolve, reject) {
_resolve = resolve;
_reject = reject;
}
},
resolve: function (data) {
window.setTimeout(_resolve, 0, data);
},
reject: function (data) {
window.setTimeout(_reject, 0, data);
}
};
}
});
}));
Hope it will be useful to someone, or if you have any feedback.
Thanks.

AngularJS Promise Callback Not Trigged in JasmineJS Test

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
};
}]);

Categories