I'm trying to test the asynchronous $resource service by Angular. Firstly note that I don't want to mock $resource, I actually want to get the results and test them.
So basically my getCountries method just uses Angular's $resource to return an array of countries.
Method
getCountries(): any {
var countries = this.$resource(
this.apiEndpoint.baseUrl,
{
'get': {
method: 'GET', isArray: false
}
});
return countries;
}
In my spec I'm having trouble with where to run Jasmine's done() method. Please read comments in the code.
Spec
describe(module', (): void => {
var one;
var SelfRegistrationResourceBuilder;
beforeEach((): void => {
angular.mock.module(
'BuilderModule',
'ngResource'
);
inject(
['Builder', (_SelfRegistrationResourceBuilder _: ISelfRegistrationResourceBuilder ): void => {
SelfRegistrationResourceBuilder = _SelfRegistrationResourceBuilder_;
}]);
});
describe('getCountries', (): void => {
beforeEach((done) => { //done is a parameter in beforeEach
SelfRegistrationResourceBuilder.getCountries().get(
(value: any) => {
console.log("no error");
one = "aValue";
done(); //Do I have to call done() here
}, (error: any) => {
console.log("error");
one = "aValue";
//done(); What about here
})
//done(); ..or here
});
it("should...", (done) => {
expect(one).toBe("aValue");
//done(); and here?
});
});
});
So my problem seems to be that when I run my code I either get the error stated in the title of this problem or if I play around with which done() methods I call then my variable named "one" is undefined, thus implying that the success and error call backs never got called. I can also prove that the success and error callbacks never get called because their console.log()s don't ever print to the console.
Lastly, when I check the network section of the developer tools in my browser I don't see any requests to the server. Could someone please try to help me out here, I think I have most of it figured out except the calling of done().
Thanks
done is for async tests. If you test is not async don't use done e.g.:
// No done needed
it("should...", () => {
expect(one).toBe("aValue");
});
For async tests call it after the async operation is complete. So :
beforeEach((done) => { //done is a parameter in beforeEach
SelfRegistrationResourceBuilder.getCountries().get(
(value: any) => {
console.log("no error");
one = "aValue";
done(); // Yes call done
}, (error: any) => {
console.log("error");
one = "aValue";
// do not call done. You most likely want to throw to show this is an error case.
})
});
Related
I have a React utility component that reads the contents of a URL:
'use strict';
export class ReadURL {
getContent = (url) => {
return new Promise((resolve, reject) => {
console.log('Promise')
let xhr = new XMLHttpRequest();
xhr.open("GET", url, false);
xhr.onreadystatechange = () => {
console.log('onreadystatechange', xhr.readyState)
if (xhr.readyState === 4) {
if (xhr.status === 200 || xhr.status == 0) {
console.log('200')
var allText = xhr.responseText;
resolve(allText);
} else {
reject('ajax error:' + xhr.status + ' ' + xhr.responseText);
}
}
};
xhr.send(null);
});
};
}
I have been trying to use Sinon's useFakeXMLHttpRequest() to stub the xhr, but no matter how I try, I can't get it to actually process - It currently passes with a false positive, without ever receiving onreadystatechange event.
I've tried with XHR and Axios packages as well as native XMLHttpRequest, with the request wrapped in a promise and not, a whole bunch of different tacks, and read untold blog posts, docs and SO questions and I'm losing the will to live... The component itself works perfectly.
I've managed to get tests working with promises and with stubbed module dependancies, but this has me stumped.
This is the test:
import chai, { expect } from 'chai';
import sinon, { spy } from 'sinon';
import {ReadURL} from './ReadURL';
describe('ReadURL', () => {
beforeEach(function() {
this.xhr = sinon.useFakeXMLHttpRequest();
this.requests = [];
this.xhr.onCreate = (xhr) => {
console.log('xhr created', xhr)
this.requests.push(xhr);
};
this.response = 'Response not set';
});
afterEach(function() {
this.xhr.restore();
this.response = 'Response not set';
});
it('should get file content from an xhr request', () => {
const readURL = new ReadURL(),
url = 'http://dummy.com/file.js',
urlContent = `<awe.DisplayCode
htmlSelector={'.awe-login'}
jsxFile={'/src/js/components/AncoaAwe.js'}
jsxTag={'awe.Login'}
componentFile={'/src/js/components/Login/Login.js'}
/>`;
readURL.getContent(url).then((response) =>{
console.log('ReadURL-test response', response)
expect(response).to.equal(urlContent);
});
window.setTimeout(() => {
console.log('ReadURL-test trigger response')
this.requests[0].respond(200,
{
'Content-Type': 'application/json'
},
urlContent
)
, 10});
});
});
The console.log('xhr created', xhr) is triggered, and output confirms that it's a sinon useFakeXMLHttpRequest request.
I have created a repo of the app with the bare minimum required to see the components functions:
https://github.com/DisasterMan78/awe-testcase
I haven't got a sample online currently, as I don't know of any online sandboxes that run tests. If I can find a service for that I'll try to add a proof of failed concept.
Help me Obi Wan-Kenobi. You're my only hope!
Well, that was fun. I got back to it today after working on an older project, but it still took my far too long. Stupid, small syntactic errors. Of course...
There were two critical errors.
Firstly, in order for the promise to complete, done needs to be passed as a parameter to the function of the test's it() function:
it('should get file content from an xhr request', (done) => {
...
}
Now we can complete the promise with done:
...
readURL.getContent(url)
.then((response) => {
expect(response).to.equal(this.response.content);
done();
})
.catch((error) => {
console.log(error);
done();
});
...
Somehow none of the documentation or articles I read seemed to flag this, but few of them were also dealing with promises, and my ExtractTagFromURL test, which also relies on promises did not require this, but I have confirmed it is absolutely critical to this test.
With the promise working properly, timeouts are not required around the respond() call:
...
this.requests[0].respond(this.response.status,
{
'Content-Type': this.response.contentType
},
this.response.content
);
...
And finally, my largest cock up, in order for the values of this for this.requests and the other shared properties to be properly set and read, the function parameter of beforeEach() and afterEach() need to use arrow syntax:
...
beforeEach(() => {
...
}
...
The reduced test case on Github has been updated, passes and can be cloned from https://github.com/DisasterMan78/awe-testcase
I guess this makes me Obi Wan-Kenobe, huh?
Feel free to ask for clarification if needed!
Is this a bad way to do this? I'm basically chaining promises, where each successful return from server, launches a new http.get() for more information BUT NOT when it errors. No more http.get()s if it causes an errorCallback!
$http.get(...).then(function() {
$http.get(...).then(function(){
$http.get(...).then(function(){}, function(){}),
function(){}); },
mainErrorCallback);
Would it make a difference if it was instead of "$http.get()" it does "ViewsService.loadViews()" inside the
$http.get(...).then( function() { ViewsService.loadViews(); }, function(){ console.log("error"); }).
EDIT: Here's what I mean, synchronously.. it seems like it works, but code needs cleanup/efficency to look a little neater:
http://jsfiddle.net/4n9fao9q/6/
(with delayed http requests): http://jsfiddle.net/4n9fao9q/26
$http.get(...).then((res) => {
//res has data from first http
return $http.get(...);
}).then((res) => {
//res has data from second http
return $http.get(...);
}).then((res) => {
//res has data from third http
}).catch((err) => {
//exploded
});
I think is cleaner. You can replace $http.get with whatever function returns a promise. If ViewsService.loadViews() returns a promise, you can use it.
As asked in the comments.
...
ViewsService.loadViews = function() {
//returns a promise
return $http.get(...);
}
OR
ViewsService.loadViews = function() {
return new Promise((resolve, reject) => {
$http.get(...).then((result) => {
//whatever
return resolve();
})
})
return $http.get(...);
}
With any of this options for loadViews you can do ViewsService.loadViers.then(etc)
Is this a bad way to do this?
Efficiency
Unless you are using the response from the first request as input to the following request then this isn't
a very efficient way to do this, as each request will be blocked until the previous one has returned. A better
way would be to use $.all($http.get(...),$http.get(...))
Style
The nested calls (the pyramid of doom) are difficult to read. As each call has the same failure response you could just chain these calls instead. e.g.
$http.get(..).then
($http.get(..)).then(
($http.get(..)).catch(errHander)
I am trying to test my Ionic app with Jasmine. This is my test suit.
beforeEach(() => {
auth = new Authentication(<any>new HttpMock(), <any>new StorageMock())
user = new MockUser();
header = new Headers({'uid': '1'});
resp = new Response( new ResponseOptions({body: {name: user.name }, headers: header}))
});
it('facebok login ',(done)=>{
spyOn(auth.http,'post').and.returnValue(HttpMock.returnValue(resp));
spyOn(Facebook,'login').and.returnValue(Promise.resolve({authResponse: {accessToken: 1}}))
auth.facebookLogin().then((res)=>{
expect(auth.http.post.calls.argsFor(0)).toEqual([CONFIG.url.facebookUrl,{}])
expect(res.authResponse.accessToken).toEqual(1);
done();
},(err)=>{
done();
});
});
My HttpMock class to mock http calls looks like this.
export class HttpMock {
static returnValue(data){
return Observable.create((observer) => {
observer.next(data);
})
}
}
The relevant part in the service I am testing is,
facebookLogin(): Promise<any>{
let permissions = ["public_profile","email"];
return Facebook.login(permissions)
.then( (response) => {
let token = { access_token: response.authResponse.accessToken };
return this.login( token ,'facebookUrl').toPromise();
}).catch( this.handleError);
login(data , urlKey): Observable<any>{
return this.http.post(CONFIG.url[urlKey], data)
.map( (res: Response) => this.saveUserInfo(res) ).catch( this.handleError)
}
saveUserInfo(res: Response): Response{
let userInfo = this.getUserInfo(res);
this.user = userInfo;
this.storage.set('user', userInfo);
return res;
}
The facebookLogin method goes like this. Access Facebook class login method which returns a promise. With information from the promise, I make http post request and save the returned data and then convert observable to promise with toPromise. In the test I spy on Facebook.login to return a resolving promise and spyOn http.post to return a successful observable. This is working fine in my app.But I am unable to run the test as it give the following error.
Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
The code runs fine till the last point in http.post.map but then is not being run in the test. I think the problem is with the toPromise in the service.
Any kind of hep would be appreciated.
From my limited knowledge on Observable , I believe the problem with the approach was due to the fact that toPromise didnt get the value from observer.next(data). I assume subscription is necessary for that. The simple approach with Observable.of worked for me. You can import it from import 'rxjs/add/observable/of'
With Sinon, I'm trying to spy on an async function call from a function in my qunit test:
test("requestLiveCategoriesData should call parseCategoriesData", function(){
var spy = sinon.spy(this.liveCategoriesModel, 'parseCategoriesData');
this.liveCategoriesModel.requestLiveCategoriesData();
sinon.assert.calledOnce(spy);
});
The test fails (expected parseCategoriesData to be called once but was called 0 times) even though parseCategoriesData does indeed get called by the requestLiveCategoriesData - I know this because parseCategoriesData called is output to the console when I run the test in the browser
This is the code I'm testing (simplified for the sake of the question):
requestLiveCategoriesData: function () {
console.log('getting live categories');
try {
console.log("--- RETRIEVING LIVE CATEGORIES EVENTS ---");
liveCategoriesCall = new LiveEventRequest(eventObjRequest);
liveCategoriesCall.on('reset', this.parseCategoriesData, this); //the spied on function is called from here
liveCategoriesCall.fetch({
success: function (collection, resp, options) {
console.log('Live Categories Events Request complete.');
},
error: function(collection, resp) {
console.log("Error on Live Categories Events Request");
if (_.has(resp, 'statusText') && resp.statusText === "timeout") {
/* Timeout error handling */
console.log("Live Categories Events Request Timeout");
}
Conf.generalNetworkError();
},
complete: function (resp, textStatus) {
console.log("Live Categories Request teardown.");
if (liveCategoriesCall) { liveCategoriesCall.off('reset', that.parseCategoriesData, that); }
},
cache:false,
timeout: that.get('liveEventsTimeout')
});
} catch(err) {
console.log("ERROR: PROCESSING LIVE CATEGORIES");
console.log(err.message);
console.log(err.stack);
if (liveCategoriesCall) { liveCategoriesCall.off('reset', this.parseEventsData, this); }
this.set({
'lastRequest': (new Date()).getTime(),
'liveCategories': []
});
this.trigger("errorAPI", err.message);
}
},
parseCategoriesData: function (liveCategoriesCall) {
console.log('parseCategoriesData called');
},
Am I going about this the correct way?
You at least need to instruct QUnit to wait on the asynchronous response call using async().
Now when you've got that set up you need to figure out when you can call sinon.assert.calledOnce(spy);. It looks like there is currently no way to know when the LiveEventRequest has returned data.
If you have no way of modifying the current code using a setTimeout to wait a bit is your only (bad) option.
If you can change your code you should probably investigate if you can return a promise from the requestLiveCategoriesData call. Have the promise resolve when the data has arrived. Then you can wait on that promise before you do the Sinon check and follow that by a done() call like in the QUnit async documentation.
And while we're at it: You probably should use a sinon fakeserver or some other way to mock the results of the LiveEventRequest as well.
My back-end call is returning undefined. A.k.a TypeError: Cannot read property 'then' of undefined. I think I am calling it incorrectly.
Here is the AngularJS controller code:
$scope.addUser = function (chaseUser) {
Accounts.addChaseUser(userToSubmit).then(function (response, err) {
if (err) {
$scope.errorMessage = "There was an error.";
$log.debug(err);
} else if (response) {
$scope.errorMessage = "It worked.";
$log.debug(response);
} else {
$scope.errorMessage = "No 'response' nor 'err' returned from backend";
}
});
};
How this responds is that if...
(1) I put in correct credentials, I get a response that comes back with all the transaction data but still TypeError: Cannot read property 'then' of undefined in the console.
(2) Input incorrect credentials, I get no error object, response object, or even making it down to the line where I have $scope.errorMessage = "No 'response' nor 'err' returned from backend"; plus, of course, `cannot read property 'then' of undefined.
Corresponding AngularJS service:
return {
addChaseUser: function(credentials) {
return Restangular.one('user').customPOST(credentials, 'addUser');
}
};
On the backend (controller):
module.exports = {
addChaseUser: function (req, res) {
PlaidService.provideCredentialsToMFA(req.body, function (err, mfaRes) {
if (err) {
return res.status(403).json(err);
}
return res.json(mfaRes);
});
},
};
Backend service:
var plaid = require('plaid');
var plaidClient = new plaid.Client('test_id', 'test_secret', plaid.environments.tartan);
module.exports = {
provideCredentialsToMFA: function (credentials, cb) {
Q.fcall(PlaidService.connectUser.bind(this, credentials))
.then(PlaidService.saveUsersAccessToken.bind(this, credentials))
.then(PlaidService.getTransactionData.bind(this, credentials))
.then(function(transactions) {
cb(null, transactions);
},
function(err) {
console.log(err);
cb(err, null);
});
},
}
How am I supposed to be calling this Restangular POST from the AngularJS controller? It should not be returning undefined.
Since you are getting the response from the server side, it means that your server side code and the angular service code is working just fine. :)
The only possibility I can see is that I is wrong with the application of .then() block is that insted of two parameters(i.e., response and err) lets try with only one parameter.
Something like following :
$scope.addUser = function (chaseUser) {
Accounts.addChaseUser(userToSubmit).then(function (response) {
if(response.status == 'error'){
$log.debug('Got error in the response');
}else{
$log.debug('SUCCESS');
}
});
};
I have used .then(function(data)) with only one parameter. That is the only thing I could find :)
Incase you still don't see it, you are missing a few returns in your functions.
addUser: function (req, res) {
return PlaidService.provideCredentialsToMFA(
// ... your code ...
);
},
provideCredentialsToMFA should return the promise
provideCredentialsToMFA: function (credentials, cb) {
return plaidClient.addConnectUser(
// ... Your code ...
);
}
EDIT:
simplifying the working code looks something like this. Notice there are no returns and only callbacks are used:
findChargeById: function (req, res) {
fetchCharge(someParams, someCallBack);
}
fetchCharge: function (chargeId, cb) {
stripe.charges.retrieve(chargeId, anotherCallBack);
}
Your code looks like this. It's similar but the $scope.addUser expects something returned and doesn't use a callback
addUser: function (req, res) {
provideCredentialsToMFA(someParams, someCallBack);
// addUser returns nothing
}
provideCredentialsToMFA: function (credentials, cb) {
plaidClient.addConnectUser(someParams, someCallBack);
// provideCredentialsToMFA returns nothing and it seems plaidClient.addConnectUser returns nothing too
}
$scope.addUser = function (userInfo) {
// Here is the difference with your 'then' call. Accounts.addUser doesn't return anything and takes 2 params
Accounts.addUser(userInfo).then(someCallBack);
};