mock api call in a custom hook using react test library - reactjs

I have written a custom hook and inside it's useEffect function I am calling an API and set the result into state. Here is my custom hook:
export const useGetUsers = (searchParams?: any | undefined, userId?: string) => {
const [users, setUsers] = useState<{
data: readonly any[] | null;
loading: boolean;
}>({
data: [],
loading: true,
});
const parsedSearchParams = {
limit: 100,
...(searchParams || {}),
};
const searchParamStr = `?${makeQueryStringFromObject(parsedSearchParams)}`;
useEffect(() => {
userRequest('users', 'get', null, searchParamStr)
.then(result => {
setUsers({
data: result?.data,
loading: false,
});
})
.catch(() => {
setUsers({
data: null,
loading: false,
});
});
}, [userId, searchParamStr]);
return { users, setUsers };
};
I want my test to get through .then(). but for some reason it does not. here is my test:
test('when the call is a success', async () => {
const spy = jest.spyOn(ES, 'userRequest');
const returnPromise = Promise.resolve({data: ['a']})
ES.userRequest = jest.fn(() => returnPromise);
const { result, waitFor} = renderHook(() => useGetUsers());
await act(() => returnPromise)
await waitFor(() => expect(spy).toHaveBeenCalled())//this fails
});
here is another try and change I made in my test, but no luck:
test('when the call is a success', async () => {
jest.mock('src/..', () => ({
...jest.requireActual('src/..'),
userRequest: jest
.fn()
.mockImplementation(() => new Promise(resolve => resolve({data: ['a']}))),
}));
const { result, waitFor} = renderHook(() => useGetUsers());
await waitFor(() => expect(ES.userRequest).toHaveBeenCalled())
});
P.S. when I mock userRequest, I expect to have the return value as I mocked. but it fails. it goes to .catch instead
I tried to use waitForNextUpdate, but no luck. I would appreciate your help

This works for me:
import { renderHook, waitFor } from '#testing-library/react';
import { useGetUsers } from '../useGetUsers';
import * as utils from '../useGetUsersUtils';
it('should work', async () => {
const mockUserRequest = jest.spyOn(utils, 'userRequest');
renderHook(() => useGetUsers());
await waitFor(() => expect(mockUserRequest).toHaveBeenCalled())
});
I am not sure where is the userRequest placed in your code. As you can see from my import it is in different file then the hook.

Related

Unit testing onSuccess for a react query call

I have a component that looks like this:
<TestComponent refetch={fn} />
Within TestComponent, I have a save button that fires off a mutation:
const handleSaveClick = () => {
const reqBody: AddHotelRoomDescription = {...}
addHotelRoomDescriptionMutation(reqBody, {
onSuccess: () => {
closeDrawer();
handleRefetch();
}
})
}
Within my test file for this component, I am trying to ensure handleRefetch gets called:
it('successfully submits and calls the onSuccess method', async () => {
const mockId = '1234'
nock(API_URL)
.post(`/admin/${mockId}`)
.reply(200);
const user = userEvent.setup();
const editorEl = screen.getByTestId('editor-input');
await act( async () => { await user.type(editorEl, 'Updating description'); });
expect(await screen.findByText(/Updating description/)).toBeInTheDocument();
const saveButton = screen.getByText('SAVE');
await act( async () => { await user.click(saveButton) });
expect(mockedMutate).toBeCalledTimes(1);
await waitFor(() => {
expect(mockedHandleRefetch).toBeCalledTimes(1); <-- fails here
})
})
I am not sure how to proceed here. I know I can test useQuery calls by doing:
const { result } = renderHook(() => useAddHotelRoomDescription(), { wrapper }
But I think this is for a different situation.
Appreciate the guidance!

How to test arguments of custom hooks with jest

I have react-native screen component that i want to test using jest and #testing-library/react-native
It looks something like this
export const SelectPaymentIdScreen = () => {
const { navigateToScreen } = useFlowNavigation();
const [orderId, setOrderId] = useState<string | undefined>(undefined);
const selectedPsp = 'test';
const [paymentError, setPaymentError] = useState<string | undefined>(undefined);
const { isLoading, mutate: getPaymentDetails } = usePaymentMutation(
{ orderId: orderId as string, psp: selectedPsp },
data => {
navigateToScreen('PAYMENT_SCREEN');
return;
}
setPaymentError('Payment provider not supported!');
},
(error: ErrorResponse) => {
setPaymentError(error.message);
},
);
return (
<... some JSX/>
);
};
I wrote my test like this:
const mockGetPaymentDetails = jest.fn();
jest.mock('../bootstrap', () => ({
useFlowNavigation: jest.fn().mockReturnValue({
navigateToScreen: jest.fn(),
}),
}));
jest.mock('../queries', () => ({
usePaymentMutation: jest.fn().mockImplementation(() => {
return { isLoading: false, mutate: mockGetPaymentDetails };
}),
}));
describe('Test SelectPaymentIdScreen', () => {
it('Renders screen correctly and checkout button is disabled when text input is empty', () => {
const { getByLabelText, getByText } = render(<SelectPaymentIdScreen />);
const input = getByLabelText('TextInputField');
const checkoutButton = getByText('CHECKOUT');
expect(input).toBeTruthy();
expect(checkoutButton).toBeTruthy();
//Checkout button should be disabled
fireEvent.press(checkoutButton);
expect(mockGetPaymentDetails).toHaveBeenCalledTimes(0);
fireEvent.changeText(input, '1234');
fireEvent.press(checkoutButton);
expect(mockGetPaymentDetails).toHaveBeenCalledTimes(1);
});
});
This works however if i run coverage report it says i am not testing this second and third arguments of usePaymentMutation.
I am not sure how to test them. I can extract second argument to a separate file but the problem is that this function depends on navigateToScreen which i need to pass it and than again i have non-tested function as the second argument.
I will try something like:
const mockGetPaymentDetails = jest.fn();
jest.mock('../bootstrap', () => ({
useFlowNavigation: jest.fn().mockReturnValue({
navigateToScreen: jest.fn(),
}),
}));
jest.mock('../queries', () => ({
usePaymentMutation: jest.fn().mockImplementation((data, successCl, errorCl) => {
return {
isLoading: false,
mutate: () => {
mockGetPaymentDetails()
successCl()
}
};
}),
}));
and in your test, you can now test that successCl was called when you call mockGetPaymentDetails. Then something similar for errorCl.

How to mock react custom hook returned value?

Here is my custom hook:
export function useClientRect() {
const [scrollH, setScrollH] = useState(0);
const [clientH, setClientH] = useState(0);
const ref = useCallback(node => {
if (node !== null) {
setScrollH(node.scrollHeight);
setClientH(node.clientHeight);
}
}, []);
return [scrollH, clientH, ref];
}
}
I want each time that it is called, it return my values. like:
jest.mock('useClientRect', () => [300, 200, () => {}]);
How can I achieve this?
Load the hook as a module. Then mock the module:
jest.mock('module_name', () => ({
useClientRect: () => [300, 200, jest.fn()]
}));
mock should be called on top of the file outside test fn. Therefore we are going to have only one array as the mocked value.
If you want to mock the hook with different values in different tests:
import * as hooks from 'module_name';
it('a test', () => {
jest.spyOn(hooks, 'useClientRect').mockImplementation(() => ([100, 200, jest.fn()]));
//rest of the test
});
Adding on to this answer for typescript users encountering the TS2339: Property 'mockReturnValue' does not exist on type error message. There is now a jest.MockedFunction you can call to mock with Type defs (which is a port of the ts-jest/utils mocked function).
import useClientRect from './path/to/useClientRect';
jest.mock('./path/to/useClientRect');
const mockUseClientRect = useClientRect as jest.MockedFunction<typeof useClientRect>
describe("useClientRect", () => {
it("mocks the hook's return value", () => {
mockUseClientRect.mockReturnValue([300, 200, () => {}]);
// ... do stuff
});
it("mocks the hook's implementation", () => {
mockUseClientRect.mockImplementation(() => [300, 200, () => {}]);
// ... do stuff
});
});
Well, this is quite tricky and sometimes developers get confused by the library but once you get used to it, it becomes a piece of cake. I faced a similar issue a few hours back and I'm sharing my solution for you to derive your solution easily.
My custom Hook:
import { useEffect, useState } from "react";
import { getFileData } from "../../API/gistsAPIs";
export const useFilesData = (fileUrl: string) => {
const [fileData, setFileData] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
useEffect(() => {
setLoading(true);
getFileData(fileUrl).then((fileContent) => {
setFileData(fileContent);
setLoading(false);
});
}, [fileUrl]);
return { fileData, loading };
};
My mock code:
Please include this mock in the test file outside of your test function.
Note: Be careful about the return object of mock, it should match with the expected response.
const mockResponse = {
fileData: "This is a mocked file",
loading: false,
};
jest.mock("../fileView", () => {
return {
useFilesData: () => {
return {
fileData: "This is a mocked file",
loading: false,
};
},
};
});
The complete test file would be:
import { render, screen, waitFor } from "#testing-library/react";
import "#testing-library/jest-dom/extend-expect";
import FileViewer from "../FileViewer";
const mockResponse = {
fileData: "This is a mocked file",
loading: false,
};
jest.mock("../fileView", () => {
return {
useFilesData: () => {
return {
fileData: "This is a mocked file",
loading: false,
};
},
};
});
describe("File Viewer", () => {
it("display the file heading", async () => {
render(<FileViewer fileUrl="" filename="regex-tutorial.md" className="" />);
const paragraphEl = await screen.findByRole("fileHeadingDiplay");
expect(paragraphEl).toHaveTextContent("regex-tutorial.md");
});
}

Validate unit test logic that runs inside a Promise.resolve

Setup
react : 16.6.0
react-native : 0.57.4
jest : 23.6.0
enzyme : 3.5.0
I have the following logic inside a component
onRefresh = () => {
const { getCustomerAccounts } = this.props
this.setState({ refreshing: true })
getCustomerAccounts()
.then(() => this.setState({ refreshing: false }))
};
which I'm trying to test is using jest like so
describe('Instance', () => {
const getCustomerAccountsMock = jest.fn(() => Promise.resolve({}))
const props = {
getCustomerAccounts: getCustomerAccountsMock,
}
const instance = shallow(<Component {...props} />).instance()
describe('onRefresh', () => {
it('should call getCustomerAccounts', () => {
instance.onRefresh()
expect(getCustomerAccountsMock).toHaveBeenCalled()
expect(getCustomerAccountsMock).toHaveBeenCalledTimes(1)
expect(getCustomerAccountsMock.mock.calls[0][0]).toBeUndefined()
})
})
})
test runs fine but I'm not able to test what happens when getCustomerAccounts().then() runs
Basically I want to test does this.state.refreshing get set to false when getCustomerAccounts().then() runs
Suggestions?
Return the Promise from onRefresh:
onRefresh = () => {
const { getCustomerAccounts } = this.props
this.setState({ refreshing: true })
return getCustomerAccounts() // <= return the Promise
.then(() => this.setState({ refreshing: false }))
};
...then you can test it like this:
describe('Instance', () => {
const getCustomerAccountsMock = jest.fn(() => Promise.resolve({}))
const props = {
getCustomerAccounts: getCustomerAccountsMock,
}
const wrapper = shallow(<Component {...props} />)
const instance = wrapper.instance()
describe('onRefresh', () => {
it('should call getCustomerAccounts', async () => { // <= async test function
await instance.onRefresh() // <= await the Promise
expect(getCustomerAccountsMock).toHaveBeenCalled()
expect(getCustomerAccountsMock).toHaveBeenCalledTimes(1)
expect(getCustomerAccountsMock.mock.calls[0][0]).toBeUndefined()
expect(wrapper.state('refreshing')).toBe(false); // Success!
})
})
})
Details
Returning the Promise lets you await it in the test.
Use an async test function so you can await the returned Promise.
Assign the wrapper to a variable so you can use it to check the state.

mock dispatch function is not called

I am writing a test for async action creator but encountered a problem where it states "Expected mock function to have been called" with:
[{"type": "GET_LOGIN_SUCCESS", "value": true}]
But it was not called.
I am not sure where exactly the problem is. If anyone could help that will be greatly appreciated.
Here's my actions.js
import { GET_LOGIN_SUCCESS } from './constants'
export const getLoginInfo = () => {
return (dispatch, getState, axiosInstance) => {
return axiosInstance.get('/api/isLogin.json')
.then((res) => {
dispatch({
type: GET_LOGIN_SUCCESS,
value: res.data.data.login
})
console.log('finishing dispatch')
})
}
}
actions.test.js
import { getLoginInfo } from './actions'
import { GET_LOGIN_SUCCESS } from './constants'
describe('async actions', () => {
it('dispatches GET_LOGIN_SUCCESS when getting login finishes', () => {
const axiosInstance = {
get: jest.fn(() => Promise.resolve({ data: { data: {login : true }}}))
}
const dispatch = jest.fn()
getLoginInfo()(dispatch, null, axiosInstance)
expect(dispatch).toHaveBeenCalledWith({
type: GET_LOGIN_SUCCESS,
value: true
})
})
})
The problem is that jest can't know that there are async task involved. So in your case you create a mock that returns a promise, and dispatch is called when the promise is resolved. As JavaScript is single threaded, it first evaluate the code in the test and all async tasks are done afterwards. So you need to make jest aware of the promise by using async/await:
describe('async actions', () => {
it('dispatches GET_LOGIN_SUCCESS when getting login finishes', async() => {
const p = Promise.resolve({ data: { data: {login : true }}}))
const axiosInstance = {
get: jest.fn(() => p
}
const dispatch = jest.fn()
getLoginInfo()(dispatch, null, axiosInstance)
await p // even it sounds not very logically you need to wait here
expect(dispatch).toHaveBeenCalledWith({
type: GET_LOGIN_SUCCESS,
value: true
})
})
As #brian-lives-outdoors points out, getLoginInfo returns the promise as well so you could also just wait for the result of the call:
it('dispatches GET_LOGIN_SUCCESS when getting login finishes', async() => {
const axiosInstance = {
get: jest.fn(() => Promise.resolve({ data: { data: {login : true }}}))
}
const dispatch = jest.fn()
await getLoginInfo()(dispatch, null, axiosInstance)
expect(dispatch).toHaveBeenCalledWith({
type: GET_LOGIN_SUCCESS,
value: true
})
})
There is a epic article that describes the whole topic

Resources