Testing concurrent requests in redux-thunk with Jest and Moxios - react-native

I am having trouble testing a function with concurrent API calls. Here's the code I want to test that relies on redux-thunk :
const loadResources = () => {
return (dispatch) => {
dispatch(setLoaderState(true))
// events
API.get('/internal/timeo/api/v0/actions')
.then(response => initialFetchEventsSuccess(dispatch, response))
.catch(error => onRequestErrorCallback(dispatch, error));
// clients
API.get('/internal/obeya/api/v0/clients')
.then(response => initialFetchClientsSuccess(dispatch, response))
.catch(error => onRequestErrorCallback(dispatch, error));
// resources
API.get('/internal/obeya/api/v0/resources')
.then(response => getRessourcesSuccess(dispatch, response))
.catch(error => onRequestErrorCallback(dispatch, error));
}
}
// on successfull fetch we dispatch data to the store
const initialFetchEventsSuccess = (dispatch, data) => {
dispatch(setLoaderState(false))
dispatch(setErrorState(false))
dispatch({
type: LOAD_EVENTS,
payload: data.data
});
}
// on successfull fetch we dispatch data to the store
const initialFetchClientsSuccess = (dispatch, data) => {
dispatch(setLoaderState(false))
dispatch(setErrorState(false))
dispatch({
type: LOAD_CLIENTS,
payload: data.data
})
}
// on successfull fetch we dispatch data to the store
const getRessourcesSuccess = (dispatch, data) => {
dispatch({
type: SET_RESOURCES,
payload: data.data
})
}
It sends concurrent request to the API and then dispatches actions to the redux store upon success. Those requests are independent so I dont really care which one gets executed first.
However when I try to test this code with moxios and redux-mock-store I only get actions dispatched from the first request in my mocked store :
it('loadsResources', async (done)=> {
moxios.stubRequest('/internal/timeo/api/v0/actions', {
status: 200,
response: getActionsMock
});
moxios.stubRequest('/internal/timeo/api/v0/clients', {
status: 200,
response: getClientsMock
});
moxios.stubRequest('/internal/timeo/api/v0/resources', {
status: 200,
response: getResourcesMock
});
const expectedActions = [
{ type: LOAD_EVENTS, payload: getActionsMock},
{ type: LOAD_CLIENTS, payload: getClientsMock},
{ type: SET_RESOURCES, payload: getResourcesMock},
]
const store = makeMockStore({});
await store.dispatch(loadResources);
setTimeout(() => {
const actions = store.getActions();
console.log(actions)
done();
}, 1000);
});
Here in actions I only get the LOAD_EVENTS action in the end, whatever timeout I set up. What am I doing wrong ?

Late to answer maybe but I came across this in moxios repo while looking for similar answer.
https://github.com/axios/moxios#mocking-a-axioscreate-instance
So, you are in the right direction. Just need to assert axios instances and get results that way.
let axiosInstance;
beforeEach(() => {
axiosInstance = axios.create();
moxios.install(axiosInstance);
});
afterEach(() => {
moxios.uninstall(axiosInstance);
});
test('Should get results', (done) => {
moxios.stubRequest(`url`, {status: 200, response: response /* or response */});
axiosInstance.get(`url`)
.then(res => res.status === 200)
.finally(done)
})

Related

How to test complex async reducers with Jest

I have such kinds of reducers that use fetch API as its base ultimately:
export const fetchRelatedFamilies = () => {
return (dispatch, getState) => {
if (isEmpty(getState().relatedFamiliesById)) {
dispatch({ type: REQUEST_RELATED_FAMILIES_BY_ID })
new HttpRequestHelper('/api/related_families',
(responseJson) => {
dispatch({ type: RECEIVE_RELATED_FAMILIES_BY_ID, updates: responseJson.relatedFamiliesById })
},
e => dispatch({ type: RECEIVE_RELATED_FAMILIES_BY_ID, error: e.message, updates: {} }),
).get()
}
}
}
Code for HttpRequestHelper is here: https://github.com/broadinstitute/seqr/blob/master/ui/shared/utils/httpRequestHelper.js
Here is how I am trying to test it (but its not working):
import configureStore from 'redux-mock-store'
import fetchMock from 'fetch-mock'
import thunk from 'redux-thunk'
import { cloneDeep } from 'lodash'
import { fetchRelatedFamilies, REQUEST_RELATED_FAMILIES_BY_ID, RECEIVE_RELATED_FAMILIES_BY_ID } from 'redux/rootReducer'
import { STATE1 } from '/shared/components/panel/fixtures.js'
describe('fetchRelatedFamilies', () => {
const middlewares = [thunk]
const testActionsDispatch = async (currstate, expectedActions) => {
const store = configureStore(middlewares)(currstate)
store.dispatch(fetchRelatedFamilies())
// need to mimick wait for async actions to be dispatched
//await new Promise((r) => setTimeout(r, 200));
expect(store.getActions()).toEqual(expectedActions)
}
afterEach(() => {
fetchMock.reset()
fetchMock.restore()
})
it('Dispatches correct actions when data - relatedFamiliesById - is absent in state', () => {
const relatedFamiliesById = cloneDeep(STATE1.relatedFamiliesById)
fetchMock
.getOnce('/api/related_families', { body: relatedFamiliesById, headers: { 'content-type': 'application/json' } })
STATE1.relatedFamiliesById = {}
const expectedActions = [
{ type: REQUEST_RELATED_FAMILIES_BY_ID },
{ type: RECEIVE_RELATED_FAMILIES_BY_ID, updates: relatedFamiliesById }
]
testActionsDispatch(STATE1, expectedActions)
})
})
I don't see { type: RECEIVE_RELATED_FAMILIES_BY_ID, updates: relatedFamiliesById } in the resulting store actions, so I tried to use the trick: await new Promise((r) => setTimeout(r, 200)); in hope that it's the issue with async fetch but what it causes is that test will pass no matter what expected actions are as if the code that is following await is completely being ignored. I can't use store.dispatch(fetchRelatedFamilies()).then(... probably because Promise is not returned, and I am getting then access of undefined error. I tried to use waitFor from the library: https://testing-library.com/docs/guide-disappearance/ but I am having really big troubles installing the library itself due to the nature of the project itself and its version, so I need to avoid it still somehow.
So, the only question that I have is how I can make the action dispatched inside the async reducer to appear, in this case - { type: RECEIVE_RELATED_FAMILIES_BY_ID, updates: relatedFamiliesById }.
The problem with the current code is that although you are awaiting for 200ms in your testActionsDispatch helper method (so that the mocked promise is resolved), you are not awaiting in the test code for that promise of 200ms to resolve.
In order to do that you have to declare your test as async and await for the execution of the testActionsDispatch code:
const testActionsDispatch = async (currstate, expectedActions) => {
const store = configureStore(middlewares)(currstate)
store.dispatch(fetchRelatedFamilies())
// need to mimick wait for async actions to be dispatched
await new Promise((r) => setTimeout(r, 200));
expect(store.getActions()).toEqual(expectedActions)
}
// Note that the test is declared as async
it('Dispatches correct actions when data - relatedFamiliesById - is absent in state', async () => {
const relatedFamiliesById = cloneDeep(STATE1.relatedFamiliesById)
fetchMock
.getOnce('/api/related_families', { body: relatedFamiliesById, headers: { 'content-type': 'application/json' } })
STATE1.relatedFamiliesById = {}
const expectedActions = [
{ type: REQUEST_RELATED_FAMILIES_BY_ID },
{ type: RECEIVE_RELATED_FAMILIES_BY_ID, updates: relatedFamiliesById }
]
// Await the execution of the helper code
await testActionsDispatch(STATE1, expectedActions)
})
Now that should work, but we are adding a delay of 200ms in every test that uses this testActionsDispatch helper. That can end up adding a lot of time when you launch your test and ultimately at a logical level is not really ensuring that the promise resolves.
A better approach is to return the promise in your reducer so we can wait for it to resolve directly in the test (I'm assuming the get method from HttpRequestHelper returns the promise created by fetch and returning it):
export const fetchRelatedFamilies = () => {
return (dispatch, getState) => {
if (isEmpty(getState().relatedFamiliesById)) {
dispatch({ type: REQUEST_RELATED_FAMILIES_BY_ID })
return new HttpRequestHelper('/api/related_families',
(responseJson) => {
dispatch({ type: RECEIVE_RELATED_FAMILIES_BY_ID, updates: responseJson.relatedFamiliesById })
},
e => dispatch({ type: RECEIVE_RELATED_FAMILIES_BY_ID, error: e.message, updates: {} }),
).get()
}
}
}
Then, in your helper you can simply await for this returned promise to resolve:
const testActionsDispatch = async (currstate, expectedActions) => {
const store = configureStore(middlewares)(currstate)
// Await for the promise instead of awaiting a random amount of time.
await store.dispatch(fetchRelatedFamilies())
expect(store.getActions()).toEqual(expectedActions)
}

axios cancellation caught inside of then() instead of catch()

I making a multi-upload file form.
Upon user cancellation, once the corresponding axios call get cancelled using cancel(), I having a weird behaviour. My axios call get caught inside the then() whereas it should be caught inside of catch(). The response inside of then() returns undefined.
I am having a hard time figuring if I did something wrong on the front-end part, I think my call is may be missing some headers or maybe it's on the backend part ?
const payload = { file, objectId: articleId, contentType: 'article' };
const source = axios.CancelToken.source();
// callback to execute at progression
const onUploadProgress = (event) => {
const percentage = Math.round((100 * event.loaded) / event.total);
this.handleFileUploadProgression(file, {
percentage,
status: 'pending',
cancelSource: source,
});
};
attachmentService
.create(payload, { onUploadProgress, cancelToken: source.token })
.then((response) => {
// cancelation response ends up here with a `undefined` response content
})
.catch((error) => {
console.log(error);
// canceled request do not reads as errors down here
if (axios.isCancel(error)) {
console.log('axios request cancelled', error);
}
});
the service itself is defined below
export const attachmentService = {
create(payload, requestOptions) {
// FormData cannot be decamelized inside an interceptor so it's done before, here.
const formData = new FormData();
Object.entries(payload).forEach(([key, value]) =>
formData.append(decamelize(key), value),
);
return api
.post(resource, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
...requestOptions,
})
.then((response) => {
console.log(response, 'cancelled request answered here as `undefined`');
return response.data;
})
.catch((error) => {
// not caught here (earlier)
return error.data;
});
},
};
cancellation is called upon a file object doing
file.cancelSource.cancel('Request was cancelled by the user');
As suggested by #estus-flask in a comment, the issue is that I was catching the error inside of the service (too early). Thank you!
export const articleService = {
create(payload, requestOptions) {
// FormData cannot be decamelized inside an interceptor so it's done before, here.
const formData = new FormData();
Object.entries(payload).forEach(([key, value]) =>
formData.append(decamelize(key), value),
);
return api.post(resource, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
...requestOptions,
});
},
};

Jest / Supertest Error - Right-hand side of 'instanceof' is not callable

When using supertest like so,
import app from "../../src/app";
import request from "supertest";
describe("GET / - a simple api endpoint", () => {
it("Hello API Request", () => {
const result = request(app)
.get("/api/location/5eda6d195dd81b21a056bedb")
.then((res) => {
console.log(res);
})
// expect(result.text).toEqual("hello");
// expect(result.status).toEqual(200);
});
});
Im getting "Right-hand side of 'instanceof' is not callable".
at Response.toError (node_modules/superagent/lib/node/response.js:94:15)
at ResponseBase._setStatusProperties (node_modules/superagent/lib/response-base.js:123:16)
at new Response (node_modules/superagent/lib/node/response.js:41:8)
at Test.Request._emitResponse (node_modules/superagent/lib/node/index.js:752:20)
at node_modules/superagent/lib/node/index.js:916:38
at IncomingMessage.<anonymous> (node_modules/superagent/lib/node/parsers/json.js:19:7)
at processTicksAndRejections (internal/process/task_queues.js:84:21) {
status: 500,
text: `"Right-hand side of 'instanceof' is not callable"`,
method: 'GET',
path: '/api/location/5eda6d195dd81b21a056bedb'
This is just with supertest, the API works when using Postman.
Rest of the code for this call,
router.get(
"/location/:id",
(req, res) => {
locationController.getLocation(req, res);
}
);
const getLocation = async (req: Request, res: Response): Promise<void> => {
const { id } = req.params;
const location = await data.readRecord(id, Location);
res.status(location.code).json(location.data);
};
const readRecord = async (id: string, model: IModel): Promise<Response> => {
try {
const response = await model.findById(id);
if (response == null) return { code: 404, data: `ID ${id} Not Found` };
return { code: 200, data: response };
} catch (error) {
return errorHandler(error);
}
};
Is there a configuration im missing for supertest and typescript?
This approach worked,
import request = require("supertest");
import app from "../../src/app";
describe("GET/ api/location/id", () => {
it("should connect retrieve record and retrieve a code 200 and json response", async () => {
const res = await request(app)
.get(`/api/location/${id}`)
expect(res.status).toBe(200);
expect(res.body._id).toBe(`${id}`);
});
});
If you don't want to use "await" in your code , you can use "done()" in callback function.
like this.
import app from "../../src/app";
import request from "supertest";
describe("GET / - a simple api endpoint", () => {
it("Hello API Request", (done) => {
const result = request(app)
.get("/api/location/5eda6d195dd81b21a056bedb")
.then((res) => {
console.log(res);
expect(res.text).toEqual("hello");
expect(res.status).toEqual(200);
done();
//done() function means this test is done.
})
});
});
Awaiting the expect call (with Jest) worked for me.
await expect(...).rejects.toThrow()

React Native fetch doesn't work in another fetch callback

If I call my api function from POINT 1, fetch method inside the api method works well. When I comment it out and call the function at POINT 2 fetch method inside the addAccount() doesn't work. There is no exception, no rejection, no request on Reactotron, even I can't find request over Charles Proxy. What is the difference and what I have to know to figure it out?
I tried with RN 0.55.2 and 0.57.5
// Auth.js typical react native component
import * as api from '../actions/api';
class Auth extends Component {
// first triggered function
loginAccount(){
// api.addAccount(); // POINT 1 - this line works well if I uncomment
fetch('https://domain-a.com/login/',{
method: 'POST',
credentials: "same-origin",
headers: {
'accept-language': 'en-US;q=1',
'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
},
body: encodeURIComponent(bodyParameters)
}).then((response) => {
console.log(response);
return response.json()
}).then(({ status, invalid_credentials }) => {
if(status == "ok"){
CookieManager.get('https://domain-a.com')
.then((cookies) => {
this.fetchAccountData(cookies);
})
})
}
fetchAccountData(cookies){
fetch('https://domain-a.com/'+cookies.user_id+'/info/',{
method: 'GET',
headers: {
'cookie': cookies
}
}).then((response) => {
return response.json();
})
.then(({ user, status }) => {
api.addAccount(); // POINT 2 - this line doesn't work
});
}
}
// api.js
// I repleaced fetch code with document example just to be clearify
export const addAccount = () => {
console.log("fetch begin"); // always works
fetch('https://facebook.github.io/react-native/movies.json')
.then((response) => response.json())
.then((responseJson) => {
console.log(responseJson); // won't works from point 2
})
.catch((error) =>{
console.error(error); // never runs
});
}
It looks like your first .then statement in the addAccount() function is missing a return statement. responseJson would be undefined without a proper a 'return response.json()' statement. Also adding brackets for better semantic formatting.
export const addAccount = () => {
console.log("fetch begin"); // always works
fetch('https://facebook.github.io/react-native/movies.json')
.then((response) => {
console.log(response); //test this response
return response.json();
})
.then((responseJson) => {
console.log(responseJson); // won't works from point 2
})
.catch((error) =>{
console.error(error); // never runs
});
}

Losing connection during a fetch causes crash in React-Native app

If the internet connection is lost during a fetch in my react-native app I get Network request failed and the app crashes.
updateClientData() {
var cachedData = null;
AsyncStorage.getItem('cachedData').then((cachedDataString) => {
cachedData = JSON.parse(cachedDataString);
})
.done(() => {
if (cachedData) {
const base64 = require('base-64');
return fetch('https://...data.json', {
method: 'get',
headers: {
'Authorization': 'Basic '+base64.encode("..."),
}
})
.then( (response) => {
// never called:
return response.json();
})
.catch( (error) => {
//Shouldn't this catch network errors? It never gets called.
console.log('caught network error');
})
.then( (responseJSON) => {
//do something with the JSON
})
}
});
},
I would love to be able to handle this gracefully rather than have it crash. Any ideas?
For some reason, moving the AsyncStorage call out of this function made it work fine. I didn't actually need it until I had the result of the fetch anyway, so I moved it.
This works now:
updateClientData() {
const base64 = require('base-64');
return fetch(clientListURL, {
method: 'get',
headers: {
'Authorization': 'Basic '+base64.encode("..."),
}
})
.then( (response) => {
return response.json();
})
.catch( (error) => {
console.log('error...')
})
.then( (responseJSON) => {
// now do something with the JSON and the data from Async Storage
}
},