Testing Chained Promises (Jasmine, React, Karma) - javascript

I have run into several situations on my present project where I have a chain of promises that I'm not sure how to deal with.
Here is the relevant code block:
return this.axios.get(path, requestOpts)
.then((response) => {console.log('did authorize: ', response); return response})
.then((response) => {
if (response.data.ok) {
window.localStorage.setItem(path, JSON.stringify(response.data));
console.log("Setting localStorage item ", path, response.data);
return response.data.payloadUrl;
} else {
console.error("Non-ok response for ", path, response.data);
const resp: DisplayTokenResponse = response.data;
//TODO: reject promise?
if (resp.status === "AUTHENTICATION_REQUIRED") {
this.axiosService.goToLoginPage(window.location + '');
}
Promise.reject(response.data.message);
}
});
My test (so far) looks like this:
describe('.authorize()', () => {
let axiosSpy: jasmine.Spy;
beforeEach((done) => {
spyOn(svc, 'keyPath').and.returnValue(path);
spyOn(svc, 'storedToken').and.returnValue(stored);
let response = {
data: {
ok: true,
message: 'test-response',
payloadUrl: 'http://payload-url.com'
}
}
spyOn(svc.axios, 'get').and.callFake(
(path:string, reqOpts:AxiosRequestConfig) => {
return new Promise(() => {
response
});
}, (e) => {
console.log(`failed`);
});
});
describe('should authorize user', () => {
it('when supplied a STRING', () => {
clientId = clientId_string;
});
it('when supplied a NUMBER', () => {
clientId = clientId_number;
});
afterEach((done) => {
svc.authorize(clientId, locationId, screenId).then((result) => {
console.log(`result ${result}`);
done();
}, (e) => {
console.log(`failed with error ${e}`);
done();
});
});
});
});
I can test one-level-down promises, but how to I set up my tests to be able to handle situations like this?

Finally got it figured out. I believe it stemmed from a confusion between creating Promise instances versus their resolvers.
The new beforeEach block looks like this:
beforeEach(() => {
spyOn(svc, 'keyPath').and.returnValue(path);
spyOn(svc, 'storedToken').and.returnValue(stored);
let axiosPromise = new Promise((resolve, reject) => {
var responseData = {
data: {
ok: true,
message: 'test-response',
payloadUrl: 'http://payload-url.com'
}
};
resolve(responseData);
});
spyOn(svc.axios, 'get').and.callFake(
()=>{
return axiosPromise;
}
);
});
My tests now pass.

Related

parsing data from an api call into a text file using axios

I am parsing data from an API call into a text file. However, I wanted to use async-await and break the call below call into 3 separate functions.
#!/usr/bin/env node
const yargs = require("yargs");
const axios = require("axios");
const fs = require("fs");
const options = yargs
.usage("Usage: -n <name>")
.option("n", {
alias: "name",
describe: "Your name",
type: "string",
demandOption: true,
})
.option("s", { alias: "search", describe: "Search Term", type: "string" })
.argv;
const greetings = `Hello ${options.name}!`;
console.log(greetings);
console.log("Here's a random joke for you: ");
const url = options.search
? `https://icanhazdadjoke.com/search?term${escape(options.search)}`
: " https://icanhazdadjoke.com/";
axios.get(url, { headers: { Accept: "application/json" } }).then((res) => {
if (options.search) {
res.data.results.forEach((j) => {
fs.appendFile("jokes.txt", "\n" + j.jokes, (err) => {});
});
if (res.data.results.length === 0) {
console.log("no joke found 😭");
}
} else {
fs.appendFile("jokes.txt", res.data.joke, (err) => {
if (err) throw err;
console.log("File Updated");
});
}
});
So the above code works absolutely fine and generates the file perfectly, however when I tried to break it into the following below functions, I just get undefined in the text file, I am not sure why this is happening.
const getJoke = async (url) => {
try {
const joke = await axios.get(url, {
headers: { Accept: "application/json" },
});
return joke;
} catch (error) {
console.error(error);
}
};
const parseJokes = (res) => {
if (options.search) {
res.data.results.forEach((j) => {
return `\n ${j.joke}`;
});
if (res.data.results.length === 0) {
console.log("no joke found 😭");
}
} else {
return res.data.joke;
}
};
const addJokeToFile = async () => {
const result = await getJoke(url)
.then((res) => {
parseJokes(res);
})
.catch((err) => {
console.error(`ERROR: ${err}`);
});
fs.appendFile("jokes.txt", result, (err) => {
console.error(err);
});
};
In the second (functional approach) addJokeToFile method, you are waiting for the promise to be resolved using both ways, await and .then, following modification to the code, might help you get through:
const addJokeToFile = async () => {
getJoke(url)
.then((res) => {
// Aside, we should also return some value from parseJokes function for "no joke found 😭" case, or return null and put a check here and only append to file when jokeString is not null.
const jokeString = parseJokes(res);
fs.appendFile("jokes.txt", jokeString, (err) => {
console.error(err);
});
})
.catch((err) => {
console.error(`ERROR: ${err}`);
});
};
Try using appendFile from 'fs/promises' so that you can stick with the async/await style. Since getJoke returns a promise I would expect result to be a Promise<string | undefined> depending on if any errors show up earlier in the chain.
const { appendFile } = require('fs/promises');
const addJokeToFile = async () => {
try {
const result = await getJoke(url);
const parsed = parseJokes(result);
await appendFile('jokes.txt', parsed);
} catch (err) {
console.error(err);
}
};

Can't send request in componentDidMount React Native

I have an issue with sending a request to backend from my componentDidMount(). Basically I need to do two things before rendering screen:
Obtain data from API call and save it to state
Send that obtained data to backend and take response values from backend.
The problem I've faced on first step is that setState() is async, and even though my array is not empty (I see it's elements in render() and componentDidUpdate fucntion) in componentDidMount() when I console.log() array it will be empty. Now, the issue is: I still need to send that state array to backend before showing the screen. But how can I do it, when it appears empty there?
I have everything working fine if I send the request from the Button element in my render function, but that's not exactly what I need. Any suggestions?
this.state = {
ActivityItem: [],
}
componentDidMount() {
this.getDataFromKit(INTERVAL); //get data from library that does API calls
this.sendDataToServer(); //sending to backend
}
componentDidUpdate() {
console.log("componentDidUpdate ", this.state.ActivityItem) // here array is not empty
}
getDataFromKit(dateFrom) {
new Promise((resolve) => {
AppleKit.getSamples(dateFrom, (err, results) => {
if (err) {
return resolve([]);
}
const newData = results.map(item => {
return { ...item, name: "ItemAmount" };
});
this.setState({ ActivityItem: [...this.state.ActivityItem, ...newData] })
})
});
And last one:
sendDataToServer() {
UserService.sendActivityData(this.state.ActivityItem).then(response => {
}).catch(error => {
console.log(error.response);
})
And here it works as expected:
<Button
title='send data!'
onPress={() => this.sendDataToServer()
} />
UPDATE
If I have like this (wrapped inside initKit function this will return undefined.
AppleKit.initKit(KitPermissions.uploadBasicKitData(), (err, results) => {
if (err) {
return;
}
return new Promise((resolve) => {
AppleKit.getSamples(dateFrom, (err, results) => {
if (err) return resolve([]);//rest is the same
you have to wait for the promise to resolve. You need something like this:
componentDidMount() {
this.getDataFromKit(INTERVAL).then(result => {
this.sendDataToServer(result); //sending to backend
}).catch(e => console.error);
}
and you can update your other function that fetches data to return it:
getDataFromKit(dateFrom) {
return new Promise((resolve) => {
AppleKit.getSamples(dateFrom, (err, results) => {
if (err) return resolve([]);
const newData = results.map(item => {
return { ...item, name: "ItemAmount" };
});
const allData = [ ...this.state.ActivityItem, ...newData ];
this.setState({ ActivityItem: allData });
resolve(allData);
});
});
}
finally, you need the 'sendData' function to not depend on state, but get a param passed to it instead:
sendDataToServer(data) {
UserService.sendActivityData(data).then(response => {
// ... do response stuff
}).catch(error => {
console.log(error.response);
});
}
Handling Multiple Requests
if the requests don't depend on each other:
componentDidMount() {
Promise.all([
promise1,
promise2,
promise3,
]).then(([ response1, response2, response3 ]) => {
// do stuff with your data
}).catch(e => console.error);
}
if the requests do depend on each other:
componentDidMount() {
let response1;
let response2;
let response3;
promise1().then(r => {
response1 = r;
return promise2(response1);
}).then(r => {
response2 = r;
return promise3(response2);
}).then(r => {
response3 = r;
// do stuff with response1, response2, and response3
}).catch(e => console.error);
}
as far as your update, it seems like you wrapped your async request in another async request. I'd just chain it instead of wrapping it:
make the initKit a function that returns a promise
function initKit() {
return new Promise((resolve, reject) => {
AppleKit.initKit(
KitPermissions.uploadBasicKitData(),
(err, results) => {
if (err) reject({ error: 'InitKit failed' });
else resolve({ data: results });
}
);
});
}
make get samples a separate function that returns a promise
function getSamples() {
return new Promise((resolve) => {
AppleKit.getSamples(dateFrom, (err, results) => {
if (err) resolve([]); //rest is the same
else resolve({ data: results });
});
});
}
chain 2 promises back to back: if initKit fails, it will go in the .catch block and getSamples wont run
componentDidMount() {
initKit().then(kit => {
return getSamples();
}).then(samples => {
// do stuff with samples
}).catch(e => console.log);
}

Unit Testing Node JS API using Jest

I'm trying to write unit test cases for NodeJS API by mocking the DB call using jest.fn(() => Promise.resolve(["FAQ 1"]));
My API factory file is something like this:
const getFAQ=(request,reply)=>{
faqs.findByCondition(request.params.filter,success=>{
reply(Response.sendResponse(true, success, ResponseMessages.SUCCESS, StatusCodes.OK));
}, error => {
log.error('ERROR : ', error);
reply(Response.sendResponse(false, error, ResponseMessages.ERROR, 400));
});
};
My Model JS containing findByCondition() is something like this:
findByCondition = (condition, success_callback, error_callback) => {
"use strict";
faq.find(condition, (err, docs) => {
if (err) {
error_callback(err);
} else {
success_callback(docs);
}
});
}
I've tried writing my unit test case as follows:
describe("test cases for FAQ Factory", () => {
utils.callAPI = jest.fn(() => 'test')
test('getFAQ Success Case', (done) => {
const request = {
params: {
filter: 'all'
},
headers: {
authorization: 'asfasfasdfas'
}
}
faqModel.findByCondition = jest.fn(() => Promise.resolve(["FAQ 1"]));
faqFactory.getFAQ(request, (result) => {
expect(result).toBeDefined();
expect(result.status_code).toBe(200);
})
})
});
I'm able to run the test case successfully but the code coverage is not covering the success or error callbacks
but the same test case works if I change my API Factory to something like this:
const getFAQ = (request, reply) => {
faqs.findByCondition(request.params.filter).then(success => {
reply(Response.sendResponse(true, success, ResponseMessages.SUCCESS, StatusCodes.OK));
}).catch(error => {
log.error('ERROR : ', error);
reply(Response.sendResponse(false, error, ResponseMessages.ERROR, 400));
});
};
Is there any way that I could write my test case so that it will cover the success/error callbacks?
Basically you will have to have the findByCondition to be overwritten if you want to test both cases and pass your callback to getFAQ
I've simplified it a little to make it clearer what I mean.
const getFAQ = (request, reply) => {
faqs
.findByCondition(request.params.filter)
.then(success => {
reply(true);
})
.catch(error => {
reply(false);
});
};
const faqs = {
findByCondition: () => {
return Promise.resolve();
}
};
it("works", () => {
jest.spyOn(faqs, `findByCondition`).mockResolvedValue({});
getFAQ({ params: { filter: "hello" } }, reply => {
expect(reply).toBe(true);
});
});
it("doesn't work", () => {
jest.spyOn(faqs, `findByCondition`).mockRejectedValue(new Error(`This fails because of error`));
getFAQ({ params: { filter: "hello" } }, reply => {
expect(reply).toBe(false);
});
});
You can see it here
Update:
I have found a solution by using mockingoose library, writing my test case as follows:
test("test to getFAQ Success", async (done) => {
const request = {
params: {
filter: 'all'
},
headers: {
authorization: testData.authorization
}
}
mockingoose(faqModel.faq).toReturn([], 'find');
const result = await promisify(faqFactory.getFAQ, request);
expect(result).toBeDefined();
expect(result.status).toEqual(200);
done();
})

sinon spy doesn't register call in a generator loop?

I want to check that a piece of code is being called, so I'm using a sinon spy to assert this. However, the spy seems to be failing, despite console.logs showing that the code has been called correctly.
I'm wondering if my function being a generator is causing my spy to misreport what it's doing.
my code (i've taken out some chunks for brevity):
isBlacklisted(release, jobUUID) {
names.forEach((name) => {
this._spawnPythonProcessGenerator(
this.IS_BLACKLISTED_SCRIPT,
name
).next().value
.then((data) => {
console.log(data);
})
.catch((err) => {
this._errorEvent(release, name, err, jobUUID);
});
}, this);
}
_errorEvent(release, name, err, jobUUID) {
console.log('got here');
}
*_spawnPythonProcessGenerator(scriptSrc, name) {
const pythonProcess = this._childProcess.spawn(
'python3',
[...arguments]
);
yield new Promise((resolve, reject) => {
pythonProcess.stderr.on('data', (err) => {
reject(err.toString());
});
pythonProcess.stdout.on('data', (data) => {
resolve(data.toString());
});
});
}
and my tests:
const Blacklist = require('../../src/Blacklist2');
const childProcess = require('child_process');
const uuid = require('uuid/v4');
describe('Blacklist', () => {
let blacklist;
beforeEach(() => {
blacklist = new Blacklist(childProcess);
blacklist.IS_BLACKLISTED_SCRIPT = './test/helpers/good.py';
});
describe('isBlacklisted', () => {
it('should call the _errorEvent for every name in a release when the blacklist application is not available', async () => {
let release = {
id: 1001,
asset_controller: {
id: 54321,
},
display_name: 'Blah',
names: [
{
id: 2001,
name: 'Blah',
},
],
};
blacklist.IS_BLACKLISTED_SCRIPT = './test/helpers/'+ uuid() +'.py';
const spy = sinon.spy(blacklist, '_errorEvent');
blacklist.isBlacklisted(release, uuid());
console.log(spy);
sinon.assert.calledTwice(spy);
spy.restore();
});
});
});
my spy reports:
notCalled: true
I'll expand my comment into an actual answer, hopefully that helps.
Your problem lies with asynchrony, not with the generator. You need isBlacklisted to return a promise you can wait on. Otherwise your assertion happens before the spy is called.
Something like this:
isBlacklisted(release, jobUUID) {
let promises = names.map((name) => {
return this._spawnPythonProcessGenerator(
this.IS_BLACKLISTED_SCRIPT,
name
).next().value
.then((data) => {
console.log(data);
})
.catch((err) => {
this._errorEvent(release, name, err, jobUUID);
});
}, this);
return Promise.all(promises);
}
Then, in your test:
return blacklist.isBlacklisted(release, uuid())
.then(() => {
sinon.assert.calledTwice(spy);
});
Also... This isn't related to your problem, but your _spawnPythonProcessGenerator method doesn't need to be a generator. You're only using the first value of it by calling next like that and calling the whole thing over again for each array item.
It will work the same if you take out the *, change yield to return, and skip the .next().value when you call it. You also probably want to rename it because it's not a generator.
_spawnPythonProcess(scriptSrc, name) {
const pythonProcess = this._childProcess.spawn(
'python3',
[...arguments]
);
return new Promise((resolve, reject) => {
pythonProcess.stderr.on('data', (err) => {
reject(err.toString());
});
pythonProcess.stdout.on('data', (data) => {
resolve(data.toString());
});
});
}
When you call it:
let promises = names.map((name) => {
return this._spawnPythonProcess(
this.IS_BLACKLISTED_SCRIPT,
name
)
.then((data) => {
console.log(data);
})
.catch((err) => {
this._errorEvent(release, name, err, jobUUID);
});
}, this);
return Promise.all(promises);

JavaScript (ES6) and fetch(): How can I throw an error so that catch triggers? (Testing it with Jest)

I thought I was decent at JavaScript until now. I want to write a helper function for my HTTP requests. I tested it with Jest. The problem is that the catch() part does not get triggered. Let me give you the test first:
it("recognizes when a response's status is not okay", () => {
fetch.mockResponseOnce(JSON.stringify({ ok: false }));
expect.assertions(1);
return getRequestWithoutHeader(fullTestUrl).catch(err => {
expect(err.ok).toEqual(false);
});
});
Maybe the test is written wrongly which fails. Anyways here is the helper function that I did write. I tried out different implementations and they all fail the test:
// Implementation one: with throw
export const getRequestWithoutHeader = fullUrlRoute =>
fetch(fullUrlRoute).then(response =>
response.json().then(json => {
if (!response.ok) {
throw Error(json);
}
return json;
}, error => error)
);
// Implementation two: with throw new
export const getRequestWithoutHeader = fullUrlRoute =>
fetch(fullUrlRoute).then(response =>
response.json().then(json => {
if (!response.ok) {
throw new Error(json);
}
return json;
}, error => error)
);
// Implementation three: With Promise.reject
export const getRequestWithoutHeader = fullUrlRoute =>
fetch(fullUrlRoute).then(response =>
response.json().then(json => {
if (!response.ok) {
return Promise.reject(json);
}
return json;
}, error => error)
);
// Implementation four: with new Promise
export const getRequestWithoutHeader = fullUrlRoute =>
new Promise((resolve, reject) => {
fetch(fullUrlRoute).then(response =>
response.json().then(
json => {
if (!response.ok) {
reject(json);
}
resolve(json);
},
error => reject(error)
)
);
});
None of these work. Some of these would return using a then in the test, but I want the promise to be thrown. I want to trigger a catch.
How do I have to write this helper function?
you can try like this
fetch(fullUrlRoute)
.then(response =>{
if (response.ok) {
return response.json();
}
else throw response
})
.then(json=> {
console.log(json);
})
.catch(error =>{
console.log(error)
});
Hope this help you
Here is what I ended up having to do:
I used jest-fetch-mock for mocking the requests.
In order for the promise to be correctly rejected, I had to overrite the init argument of the mockResponseOnce function.
Here is how the test ended up looking:
it("recognizes when a response's status is not okay", () => {
fetch.mockResponseOnce(JSON.stringify({ someResponse: "someResponse" }), { status: 403 });
expect.assertions(1);
return getRequestWithHeader(fullTestUrl, article).catch(err => {
expect(err.someResponse).toEqual("someResponse");
});
});
By setting the status explicitly, it automatically sets ok: false in the response, triggering the function.
I also applied CertainPerfomance's tips and refactored the function like this:
export const getRequestWithoutHeader = fullUrlRoute =>
fetch(fullUrlRoute)
.then(response => {
if (!response.ok) {
return Promise.reject(response);
}
return response.json();
})

Categories

Resources