Test custom hook with multiple async requests with jest and renderhook - reactjs

I've been working on an application that uses react-query for handling and caching responses to our backend. Got a lot of inspiration from the creator behind that framework to use custom hooks for handling backend states. In the beginning it was quite easy but later on the hooks started to be dependent to one and another. This resulted in some wierd behavior in the CI were some testcases failed due to heavy load on the servers. (note that backend is mocked with msw)
Custom hook containing multiple custom hooks:
const useFooMutation = () => {
const cache = useQueryClient();
const { types } = useTypes(); // async call to backend
const { user } = useAuth(); // async call to backend
return useMutation(['key'], mutationFunction, {
onSuccess: async (data, variables) => {
// Do cache handling by using values types and user
},
});
};
Test case:
test('useFooMutation should foo', async () => {
const { wrapper } = setupCache();
const { result, waitFor, waitForNextUpdate } = renderHook(() => useFooMutation(), { wrapper });
await waitForNextUpdate();
act(() => {
result.current.mutate({
values
});
});
await waitFor(() => result.current.isSuccess && !result.current.isLoading);
expect(result.current.isSuccess).toEqual(true);
expect(result.current.data).toEqual(fooMockData);
});
When I debugged the code I saw that sometimes the values from the hooks useTypes and useAuth was not yet fetched. The values from the other custom hooks are used when I do cache manipulation. So this results in some wierd behavior for some machines. For example running locally this runs ok most of the times but on a heavy loaded CI server, it results in crashes quite often.
What I've tried so far is to add a timeout before the mutation function in the test case on a timeout that seems to work, not a nice solution. And by using the waitForNextUpdate the test case works fine most of the times but not when the CI is loaded.
So I need a good way to find out when then the useFooMutation has gathered all values from the hooks its dependent on before I can call mutate()?

Related

React repeatedly requests API data

I'm requesting API data through axios using RapidAPI's Apis.
I followed all the documentations provided in RapidAPI with a relatively simple code. However, when I log the values, it keeps repeatedly requesting data over and over and this in turn hikes up my requests from RapidAPI and uses a lot of resources and money. I can't figure out how to only retrieve the values ONCE. Here are my codes.
My React .tsx file
const [sportsData, setSportsData] = useState()
const fetchSportsData = () => {
axios
.request(testApi)
.then((response) => {
setSportsData(response.data)
})
.catch((error) => {
console.log(error)
})
}
fetchSportsData()
console.log(sportsData)
My Api File
export const testApi = {
method: 'GET',
url: 'https://api-football-v1.p.rapidapi.com/v3/timezone',
headers: {
'X-RapidAPI-Key': '-------removed on purpose ---------',
'X-RapidAPI-Host': 'api-football-v1.p.rapidapi.com'
}
}
I am just setting the data using useState but it seems to repeatedly rerender whenever a value is stored. I've tried many roundabout ways but it seems to repeatedly request over and over again. Has anyone used API's from RapidAPI before?
While I don't know why useState will still repeatedly retrieve API data with axios, this is a workaround as commented by Sergey Sosunov.
On the React File
const [sportsData, setSportsData] = useState()
const fetchSportsData = () => {
axios.request(testApi).then((response) => {
setSportsData(response.data)
})
.catch((error) => {
console.log(error)
})
}
useEffect(()=> {
fetchSportsData()
},[])
On dev mode, the useEffect will run twice on load and depending on your API provider, this may mean calling the API twice on every load which may double your costs unnecessarily, this only happens in development mode as outlined in react documentation, what you can do is include a useRef variable.
const firstRender = useRef(false)
useEffect(()=>{
if (firstRender.current) {
fetchSportsData()
} else {
firstRender.current = true;
}
},[])
Remember that this code may not execute perfectly when in production as useEffect is only run once and the above code needs it to run twice in order to change the state of firstRender to true before retrieving the API call. This is just to help you lower your development costs.

What is the best way to execute a function after TWO or more async functions finish in React?

I have two functions that run asynchronously getting data from the API. Both of them are called from their own useEffect().
I have a third function that needs to run once those two functions have been fully completed.
How can this be accomplished?
Edit:
Both of the async functions look like this:
useEffect(() => {
fetchBudgetBucketsData();
}, [fiscalYear]);
useEffect(() => {
fetchBudgetBucketsData();
}, [fiscalYear]);
const fetchBudgetsData = async () => {
setIsFetchingBudgets(true);
const res = await getBudgets(orgID, `${parseInt(fiscalYear)}`, '', budgetType);
setIsFetchingBudgets(false);
if (isErrorResponse(res)) {
console.warn(res.details);
message.error(res.displayText);
return;
}
setBudgets(res.budgets);
};
const fetchBudgetBucketsData = async () => {
setIsLoadingBudgetBuckets(true);
if (orgID === undefined) {
return;
}
const res = await getBudgetBuckets(orgID, fiscalYear);
setIsLoadingBudgetBuckets(false);
if (isErrorResponse(res)) {
console.warn(res.details);
message.error(res.displayText);
return;
}
setBudgetBuckets(res.buckets);
};
Whenever the budget data or bucket data is updated, I want to call another function that checks for errors. However when the page loads, I need it to wait for both of those functions to be finished before it checks for errors.
Edit #2:
After some debugging, it looks like the issue might have to do with when React updates the state. Since I am trying to check for errors in data saved in the state.
One way could be chaining Promises.
Promise.all([ApiCall1, ApiCall2])
// At this point two promises above will be resolved
.then(() => ApiCall3)
Read more
I discovered the issue was caused by how React chooses when to update the state and not how I was calling these functions asynchronously.
I was able to call my Error check function by hooking it into the output of the data fetch calls. This makes sure that the error check only runs when either the budgets or buckets are edited and finished being changed.
useEffect(() => {
getWarningsAndErrors();
}, [budgets, budgetBuckets]) //Update errors whenever we edit budgets or buckets

Unit Tests for async code (indexedDB) inside React Component

I am trying to write unit tests for a React component - That looks something broadly as follows -
a handle to a db is passed to a component
The component also receives data as props (that will change) - perhaps this is something that could come through a Redux store.
Now what I do in my useEffect for this data change is - Write into indexDB (actually using the idb library) and then on success, I read some of this data and setup a local state using setData and use this state to render
Basically all of the above works - so there is no problem with that (It's perhaps not right, but works!). My challenge is how to write unit test for this code. I have looked at using async inside jest.
The code looks something like as follows -
const Component = ({db, data}) => {
useEffect(() => {
const saveDataToDB = (data, successCB) => {
const tx = db.transaction("store", "readwrite");
const transactions = [tx.store.add(data)];
transactions.push(tx.done);
Promise.all(transactions).then((res) => {
successCB();
});
const cb = () => {
// Do somethingl
};
saveDataToDb(data, cb);
};
}, [data]);
// Rendering part.
return (
<div id="component-div">
</div>
);
};
export default Component;
and I am trying to write the tests for this component using something like following
describe('Component Tests', () => {
it("Component Renders with Data", () => {
const wrapper = mount(<Component data={data} db={db}/>);
expect(wrapper.find("#component-div").toEqual(true);
});
});
May be - there's something wrong the way component is implemented, that makes unit testing challenging. But it works, so not sure in general about what's the strategy for unit testing components that could have a few promise chains to resolve?
react testing library has support for async APIs. They can be used to achieve to solve the problem mentioned above.
This would require converting the test cases to async test cases and then awaiting using right the async APIs as described above.
The original test cases mention enzyme APIs, instead of that react-testing-library APIs are to be used.

Using Jest Mock API Calls to SetState

I have an API call which runs whenever a certain component mounts. If this API call is successful the response data is used to update the state of one of my React Hooks.
The issue I am having is either related to asynchronicity or a poorly formatted mock API call, but no matter what I try I cannot get this test to work.
Here is a simplified version of the API:
const getOrg =() => {
axios.get(URL, config)
.then(response => response.data)
.then(data => {
setOrgTitle(data.name)
}
}
Basically the API is triggered and my setOrgTitle hook is updated from the response.
const [orgTitle, setOrgTitle] = useState("");
Now in my return statement I am displaying the value of orgTitle:
<h1 className={styles.titleText} id="document-folders-h1">
{orgTitle} Folders
</h1>
Alright, so the component is pretty simple. When I am trying to test things my two ideas were to either set the initial orgTitle hook state in my test or to mock the API. After some research I decided mocking the API was the way to go.
So I have a mockAxios component:
const mockAxios = {
get: jest.fn(() => Promise.resolve({ data: {} }))
};
module.exports = mockAxios;
And my test file:
import mockAxios from "../../mockTests/DocumentFoldersMock";
it("fetches results for getAdminOrg", () => {
axios.get.mockImplementation(() =>
Promise.resolve({ data: { name: "GETtest" } })
);
const wrapper = mount(
<AppProviders>
<DocumentFolders />
</AppProviders>
);
const orgTitle = wrapper.find("#document-folders-h1");
expect(orgTitle.text()).toContain("GETtest Folders");
});
I am mocking the response data, however I am not sure how to run the setOrgTitle function which is called in the .then of my actual axios call. How can I do this from my mock axios call using my mock response?
The result of the Jest test says expected("GETtest Folders") received(" Folders") so I am pretty sure that I am either having an issue with asynchronicity or an issue calling the hook update.

How to mock client.readQuery from Apollo 2 in Jest

There's a component which I am using the cache that I created in apollo, stored inside the client.
At some point, as a callback from an input, I do the following call:
const originalName = client.readQuery<NamesCached>({ query: NamesCached })!.names
this returns directly an array of objects.
However, I do not know how to test this in my component. My first assumption was to mock exclusively the client.readQuery to return me an array from my __fixtures__. That didn't work.
Here is my best shot to mock it:
it('filters the name in the search', () => {
jest.doMock('#/ApolloClient', () => ({
readQuery: jest.fn()
}))
const client = require('#/ApolloClient')
client.readQuery.mockImplementation(()=> Promise.resolve(names()))
const { getByPlaceholderText, queryByText } = renderIndexNames()
const input = getByPlaceholderText('Search…')
fireEvent.change(input, { target: { value: 'Second Name' } })
expect(queryByText('Title for a name')).not.toBeInTheDocument()
})
For this test I am using react-testing-library. This is my best shot so far...
jest.doMock('#/ApolloClient', () => ({
readQuery: jest.fn()
}))
const client = require('#/ApolloClient')
client.readQuery.mockImplementation(()=> Promise.resolve(names()))
It is important that relies on a single IT, not on the top since there are other tests that are using the client and works properly.
With That I could had mocked in the test when invokin client.readQuery() but then in the component itself goes back to its own original state
My goal
Find a way that the client.readQuery can be mocked and return the data I am looking for. I thought in mocking, but any other solution that can work for a single or group of tests( without all of them) would be more than welcome.
I tried as well mocking on the top, but then the others are failing and i couldn't reproduce to go back to the real implementation

Resources