I have a function that looks like the following:
const func = () =>
new Promise((resolve, reject) =>
asyncFunc()
.then(data =>
axios.post('someAPI', {
data
})
)
.then(response => {
asyncFunc1(response.data)
resolve(response.data)
})
.catch(err => reject(err)
);
I have read through the documentation of jest and mocking async functions, but it does not seem to cover the scope of this function. Can someone provide some insight into testing functions of this complexity?
EDIT:
Here is the function and current testing:
export const refreshAuth = () =>
new Promise((resolve, reject) =>
getRefreshToken()
.then(refreshJWT =>
axios.post(`${SomeAPI.auth}/refresh`, {
refreshJWT
})
)
.then((res: AxiosResponse<JWTData>) => {
onSignIn(res.data.accessJWT, res.data.refreshJWT);
resolve(res.data.accessJWT);
})
.catch(err => {
console.error('failed to refresh the access token', err);
reject(err);
})
);
export const onSignIn = (access: string, refresh: string) =>
Promise.all([
SecureStore.setItemAsync(REFRESH_KEY, refresh),
SecureStore.setItemAsync(ACCESS_KEY, access)
]);
export const getRefreshToken = () => SecureStore.getItemAsync(REFRESH_KEY);
And Testing:
describe('OAuth2', () => {
it('Runs all promise chains within refreshAuth()', async done => {
const data = {
data: {
accessJWT: 'token',
refreshJWT: 'refresh'
}
};
const getItemAsync = jest.fn().mockResolvedValue({ refreshToken: 'refresh' });
const mockOnSignIn = jest.fn().mockResolvedValue({});
const mockPostAxios = mockedAxios.post.mockResolvedValue(data);
return OAuth2.refreshAuth().then(() => {
expect(getItemAsync).toHaveBeenCalled();
expect(mockPostAxios).toHaveBeenCalled();
expect(mockOnSignIn).toHaveBeenCalledWith(
data.data.accessJWT,
data.data.refreshJWT
);
});
});
});
Related
I have three async functions but the third function is running before the second one resolves. What is wrong here?
async function getSet () {
//get settings from asyncstorage and setstate
}
async function pairs () {
//fetch data and set another state
}
async function fetchIt () {
//fetch another data and set a third state
}
useEffect(() => {
getSet()
.then(pairs())
.then(fetchIt())
.then(() => {
setLoading(false);
});
}, []);
fetchIt() is running before pairs()
The calls aren't chained properly. To make it simpler use async await:
useEffect(() => {
(async function () {
await getSet();
await pairs();
await fetchIt();
setLoading(false);
})();
}, []);
If each call depends on the result of the last it looks like
const r1 = await getSet();
const r2 = await pairs(r1);
// etcetera
You haven't chained the 'then's properly. They must return a promise as well. Read more here.
const getSet = async() => {
//get settings from asyncstorage and setstate
return new Promise((resolve, reject) => {
resolve('getSet');
});
}
const pairs = async() => {
//fetch data and set another state
return new Promise((resolve, reject) => {
resolve('pairs');
});
}
const fetchIt = async() => {
//fetch another data and set a third state
return new Promise((resolve, reject) => {
resolve('fetchIt');
});
}
getSet()
.then(response => {
console.log(response);
return pairs();
})
.then(response => {
console.log(response);
return fetchIt();
})
.then(response => {
console.log(response);
// setLoading(false);
});
I making an api call using Promise.all as below:
Promise.all(this.hostName.slice(0, this.Id.length).map((hostName) => {
return this.serviceC.status(hostName)
.then(res => {
return new Promise((resolve, reject) => {
const oretry: ORInterface = {
oQid: res.rows[0].qid,
reason: this.reason
};
this.serviceB.retry(oretry).subscribe(resolve);
});
});
}))
.then(() => {
this.dialog.close();
})
.catch(err => {
console.log(err);
});
The above code is working fine.
Now I want to make another api call after the successful completion of this.serviceB.retry(oretry).
The second api is this.serviceB.createDbEntry(sentry) and sentry looks as below:
const sretry: SDInterface = {
hostName,
Id: this.Id.slice(0, this.Id.length),
reason: this.reason
};
And, I am doing it as below
Promise.all(this.hostName.slice(0, this.Id.length).map((hostName) => {
return this.serviceC.status(hostName)
.then(res => {
return new Promise((resolve, reject) => {
const oretry: ORInterface = {
oQid: res.rows[0].qid,
reason: this.reason
};
const sretry: SDInterface = {
hostName,
Id: this.Id.slice(0, this.Id.length),
reason: this.reason
};
this.serviceB.retry(oretry).subscribe(resolve);
this.serviceB.createDbEntry(sentry).subscribe(resolve);
});
});
}))
.then(() => {
this.dialog.close();
})
.catch(err => {
console.log(err);
});
The above code is giving an error:
error: "SequelizeValidationError: string violation: Id cannot be an array or an object"
It is looks like it is not calling the second api for every Id
You may want to take a look a forkJoin
import { Observable, forkJoin } from 'rxjs';
And then
ngOnInit() {
let one = this.http.get('some/api/1') //some observable;
let two = this.http.get('some/api/2') // another observable;
forkJoin([one, tow]).subscribe(response => {
// results[0] is our one call
// results[1] is our second call
let var1 = response[1];
let var2 = response[0];
}/*, error => { in case error handler } */);
}
Wouldn't it be better to use Promise.all() once more?
Promise.all(this.hostName.slice(0, this.Id.length).map((hostName) => {
return this.serviceC.status(hostName)
.then(res => {
return new Promise((resolve, reject) => {
const oretry: ORInterface = {
oQid: res.rows[0].qid,
reason: this.reason
};
this.serviceB.retry(oretry).subscribe(resolve);
});
})
.then(() => {
return Promise.all(this.Id.slice(0, this.Id.length).map(id => {
return new Promise((resolve, reject) => {
const sretry: SDInterface = {
hostName,
Id: id,
reason: this.reason
};
this.serviceB.createDbEntry(sentry).subscribe(resolve);
});
})
});
}))
.then(() => {
this.dialog.close();
})
.catch(err => {
console.log(err);
});
And using toPromise() will make the code more concise.
Promise.all(this.hostName.slice(0, this.Id.length).map((hostName) => {
return this.serviceC.status(hostName)
.then(res => {
const oretry: ORInterface = {
oQid: res.rows[0].qid,
reason: this.reason
};
return this.serviceB.retry(oretry).toPromise();
})
.then(() => {
return Promise.all(this.Id.slice(0, this.Id.length).map(id => {
const sretry: SDInterface = {
hostName,
Id: id,
reason: this.reason
};
this.serviceB.createDbEntry(sentry).toPromise();
})
});
}))
.then(() => {
this.dialog.close();
})
.catch(err => {
console.log(err);
});
Use combineLatest, in Angular we use RxJs not promises.
combineLatest(
[this.http.get('call1'), this.http.get('call2')]
).subscribe(([result1, result2]) => {
// do stuff with result1 and result2
});
promise.all takes input in an array and gives response in an array,
Create 2 functions each with your asynchronous logic returning a promise,
Say funcA and funcB, then use below to invoke them parellely
Promise.all([funcA(this.hostName), funcB(this.id)])
.then(respones => {
console.log(responses[0]); //return value for funcA
console.log(responses[1]); //return value for funcB
})
.catch(err => console.log(err));
I am assuming your logic of functions are correct, I just copy-pasted from your question and gave them structure
const funcA = (hostName) => {
hostName.slice(0, this.Id.length).map((hostName) => {
return this.serviceC.status(hostName)
.then(res => {
return new Promise((resolve, reject) => {
const oretry: ORInterface = {
oQid: res.rows[0].qid,
reason: this.reason
};
this.serviceB.retry(oretry).subscribe(resolve);
});
});
});
}
const funcB = (Id) => {
Id.slice(0, this.Id.length).map(id => {
return new Promise((resolve, reject) => {
const sretry: SDInterface = {
hostName,
Id: id,
reason: this.reason
};
this.serviceB.createDbEntry(sentry).subscribe(resolve);
});
})
}
I am trying to refactor this code using try-catch blocks:
export const authorizeConnectyCube = async (accessToken) => {
const userCredentials = {
provider: 'firebase_phone',
'firebase_phone[project_id]': "xxxxxxxx",
'firebase_phone[access_token]': accessToken,
};
await createSession();
return new Promise((resolve, reject) => {
ConnectyCube.login(userCredentials, (error, user) => {
user ? resolve(user) : reject(error);
})
}).catch(error => console.log(error));
}
const createSession = () => {
return new Promise((resolve, reject) => {
ConnectyCube.createSession((error, session) => {
session ? resolve(session.user) : reject(error)
})
}).catch(error => console.log(error));
}
However I'm not getting the same result - the asynchronousity seems to be being handled differently. Here is my attempt at refactoring:
export const authorizeConnectyCube = async (accessToken) => {
const userCredentials = {
provider: 'firebase_phone',
'firebase_phone[project_id]': "xxxxxxxxxx",
'firebase_phone[access_token]': accessToken,
};
await createSession();
try {
ConnectyCube.login(userCredentials, (error, user) => {
return user;
})
}
catch (error) {
console.log(error)
}
}
const createSession = () => {
try {
ConnectyCube.createSession((error, session) => {
return session.user
})
} catch (error) {
console.log(error);
}
}
Is there any particular part of what I'm wrong? Thanks.
Callback-based APIs don't readily turn into something you can use for async/await (which under the hood uses promises). You'll have to "promisify" them first (i.e. wrap them in promises).
Here's an example of what I'm trying to say:
// Promisify these callback-based APIs.
const login = userCredentials => {
return new Promise((resolve, reject) => {
ConnectyCube.login(userCredentials, (error, user) => {
user ? resolve(user) : reject(error);
})
})
})
const createSession = () => {
return new Promise((resolve, reject) => {
ConnectyCube.createSession((error, session) => {
session ? resolve(session.user) : reject(error)
})
})
})
// Then use them in an async function
export const authorizeConnectyCube = async (accessToken) => {
const userCredentials = {
provider: 'firebase_phone',
'firebase_phone[project_id]': "xxxxxxxx",
'firebase_phone[access_token]': accessToken,
}
try {
await createSession()
return login(userCredentials)
} catch (e) {
console.warn(e)
}
}
Also, async functions return promises, with the resolved value being the return value, and the rejected value being any uncaught error thrown inside. A value wrapped in a promise as return value for an async function is redundant.
If you're using Node 8+, it has a utility called promisify which accepts a callback-based API and returns a promise-returning version of it.
How do I return my result please,
I get an error
Can't find variable: convertToArray
I have a function in my api.js
export function apiGet() {
return axios
.get('http')
.then(res => {
const convertToArray = []
for (const key in res.data) {
convertToArray.push({ ...res.data[key], id: key })
//console.log confirms my convertToArray has the info I expect in it
}
return convertToArray;
})
.catch(e => {
console.log(e);
})
}
in my vuex store I have
// Get list of cases
loadCasess ({ commit, context }) {
return new Promise ((resolve, reject) => {
apiGet()
resolve(commit('LIST_CASES', convertToArray))
})
.catch(e => {
console.error(e)
// reject('/')
})
},
You're receiving this error because convertToArray doesn't exist in the context of your Vuex Store. You're returning it, but doesn't mean that on the function that calls the apiGet() it will exist. You should write like:
// Get list of cases
loadCases ({ commit, context }) {
return new Promise ((resolve, reject) => {
const convertToArray = apiGet()
resolve(commit('LIST_CASES', convertToArray))
}).catch(e => {
console.error(e)
// reject('/')
})
}, // ...
I'm trying to do some enzyme/jest unit testing for a asynchronous function in my reactJS component, which gets injected as prop.
My Problem is to test for a value in the then() part of the function and to test for catch() if an error occures.
This is how the function of my component (<CreateAccount />) looks like:
_onSubmit = (event) => {
event.preventDefault()
const { username, password } = this.state
this.props.createUserMutation({
variables: { username, password }
}).then(response => {
const token = response.data.createUser.token
if (token) {
Cookies.set('auth-token', token, { expires: 1 })
}
}).catch(error => {
console.warn(error)
})
}
The first test should check for .catch(error => {}) as data is undefined:
it('_onSubmit() should throw error if data is missing', () => {
const createUserMutation = () => {
return Promise.resolve({})
}
const wrapper = shallow(<CreateAccount createUserMutation={createUserMutation} />)
wrapper.update().find(Form).simulate('submit', {
preventDefault: () => {}
})
const state = wrapper.instance().state
expect(wrapper).toThrow() // <-- How to do it correctly?
})
And the second test should check if cookie is set correctly. Here I don't know how to do that? I think I have to mock Cookie
it('_onSubmit() should get token', () => {
const createUserMutation = () => {
return Promise.resolve({
data: {
createUser: { token: 'token' }
}
})
}
const wrapper = shallow(<CreateAccount createUserMutation={createUserMutation} />)
wrapper.find(Form).simulate('submit', {
preventDefault: () => {}
})
// How to check for token value and Cookies?
})
What I usually have to do when I want to see if the spy worked on the catch or then, is to add another then() on the test. For example:
it('_onSubmit() should throw error if data is missing', () => {
const createUserMutation = jest.fn(() => Promise.reject(new Error()));
const spy = jest.spyOn(console,"warn");
const wrapper = shallow(<CreateAccount createUserMutation={createUserMutation} />)
wrapper.update().find(Form).simulate('submit', {
preventDefault: () => {}
});
return createUserMutation.catch(() => {
expect(wrapper).toMatchSnapshot();
})
.then(() => {
expect(spy).toHaveBeenCalledTimes(1);
});
})
I guess it is somehow related to how NodeJS handles the queues, promises, ticks, etc, internally.
That is the rejected/catch branch. If you want to test the IF path, just use a Promise.resolve and return a promise.then() instead of catch.
Why are you using console.warn for an error? Use console.error instead. You will need to mock it out to a spy as well to test it.
First test:
it('_onSubmit() should throw error if data is missing', (done) => {
const createUserMutation = () => new Promise();
const wrapper = shallow(<CreateAccount createUserMutation={createUserMutation} />)
wrapper.update().find(Form).simulate('submit', {
preventDefault: () => {}
})
const state = wrapper.instance().state
createUserMutation.resolve().then(() => {
expect(console.warn).toHaveBeenCalled();
done();
});
})
If you are running this in a mock browser environment and not a real browser then you must mock out Cookies.set.
Second test:
it('_onSubmit() should get token', (done) => {
const createUserMutation = () => new Promise();
const wrapper = shallow(<CreateAccount createUserMutation={createUserMutation} />)
wrapper.find(Form).simulate('submit', {
preventDefault: () => {}
});
jest.spyOn(window.Cookies, 'set');
const response = {
data: {
createUser: { token: 'token' }
}
}
createUserMutation.resolve(response).then(() => {
expect(window.Cookies.set).toHaveBeenCalled();
done();
});
})
afterEach(() => {
// Reset the spies so that they don't leak to other tests
jest.restoreAllMocks();
});