Angular / Karma - $http.get not executing - javascript

I have an AngularJS project which uses Karma to run some unit tests in the browser. I'm using mocha as the test framework.
However, I've got some specification tests which need to read some JSON files and check that they adhere to a given convention spec (types, name convention etc).
I should make it clear that it is the actual contents of these files that I want to test. Not a spoofed version of them through Angular Mock's $httpBackend.
I'm marking the JSON files for serving in karma.conf.js.
files: [
{ pattern: 'static/assets/json/cards/*.json', included: false, served: true },
'path/to/angular.js',
'path/to/angular-mocks.js',
'tests/**/*.js'
]
If I run karma start, I can browse over to /base/static/assets/json/cards/something.json and see that the files are being served.
Next, in my test, both the $http and the $q services are injected.
var $http, $q;
beforeEach(module('chai'));
beforeEach(inject(function(_$http_, _$q_) {
$http = _$http_;
$q = _$q_;
}));
Then I try to load each resource using $http.get. Finally, the promises returned from $http.get are collated and a call to $q.all is made in order to wait for them all to be done, before calling done() and moving on.
it('should load the resources', function(done) {
var promises = ['admissions.json', 'discharge.json']
.map(function(resource) {
console.log('Loading', resource);
return $http.get('/base/static/assets/json/cards/' + resource);
});
$q.all(promises)
.then(function(card) {
console.log('Success');
done();
}, function(err) {
console.log('Failure', err);
done();
});
});
When my tests run, I see following console output:
Loading admissions.json
Loading discharge.json
Error: timeout of 2000ms exceeded. Ensure the done() callback is being called in this test.
At first I assumed that it might have been exceeding the timeout by taking a long time to load, but the file is only 95kb.
Next, I wondered whether the custom promise interface (.success and .error) for $http was breaking the $q.all method. But apparently not.
Finally I tried to make a standalone request for /base/static/assets/json/cards/admissions.json at the beginning of all the tests.
It returns a promise, as expected, but it is never resolved, because no response is sent back. I checked the network tools to see what was coming back and it turns out that the request isn't even made in the first place. The code definitely runs, but for some reason $http doesn't actually make the request.
My inclination is that this is something to do with Angular Mocks intercepting $http requests for it's own $httpBackend service. How can I circumvent this?

I found a solution in this blog. The problem it's that you have to add the digest of the scope, even if you are not testing controllers.
it('does a thing one way', function() {
var value;
deferred.promise.then(function(_value_) {
value = _value_;
});
deferred.resolve(10);
expect(value).not. toBe(10); // not yet 10 because $digest hasn't run
$scope.$digest();
expect(value).toBe(10); // 10 because $digest already ran
});

Related

JS mock promise on http call [duplicate]

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

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

Calling web services from Jasmine Test

I am attempting to write some black box tests to some web services. In an attempt to do this, I'm using Jasmine. I would like to know how to call a web service from Jasmine. I currently have the following setup:
describe('Web Service', function () {
describe('-> endpoint', function() {
describe('-> create', function() {
var response = null;
beforeEach(function(done) {
// execute a call to the web service here
if (done) {
done();
}
});
it('should run correctly', function(done) {
if (done) {
done();
}
});
});
});
});
How do I call a web service from Jasmine?
Thank you!
You should use something like nock to mock a response and assert that your code has called the service and received the correct response and then did its job correctly, or, if you really want nock can even allow your code to perform the actual call to the web service.
Otherwise if you just want to call the webservice from within your test you most likely have an API client somewhere where you can just perform the call yourself.
describe('My Test', function (done) {
beforeEach(function () {
var myApiClient = new MyApiClient();
//unless you're using promises
this.response = myApiClient.synchronousGet('http://someURL.com');
});
});
Jasmine itself isn't, and shouldn't be, responsible for calling your web services, that should come from within your unit under test.
You shouldn't send ajax requests from your tests because you'll be dependant of the services to run your tests... And a bad response from your webservices doesn't absolutely mean that your code is not running correctly.
A good way to do it is to mock the ajax request (as limelights said), but Jasmine has an ajax module ready to help you with this. See http://jasmine.github.io/edge/ajax.html
This Jasmine plugin will allow you to catch the calls to a service and replace it with your fresh configured response that will be always the same (because you should know the exact response to test correctly your service.
Anyway, if you absolutely want to call your service, I think you'll have to code it in JS in your tests (not recommanded to write such code in your tests. Better write function that will be tested too). As I know, Jasmine doesn't provide you this kind of helper, it's just for tests purposes.

The promise of a promise again (Angular JS)

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".

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.

Categories