How to test arguments of custom hooks with jest - reactjs

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.

Related

Jest-React hook Testing : How to call a setState inside a custom function using useEffect

I am trying to set mockPatient data and wanted to test if the 'sortByCaseFn ' function is called by the useEffect.
Here is my sourcecode:
Patient.tsx
const [patients, setPatients] = useState([]);
const [sortBy, setSortBy] = useState('events');
const [fetched, setFetched] = useState(false);
const dispatch = useAppDispatch();
const props = useAppSelector((state) => state.myPatientProps);
const getPatientData = (): void => {
dispatch(MyPatientActions.getMyPatientsData());
};
const sortByCaseFn = (sortBy, list) => {
let patientsToSort = [...list];
if (sortBy.includes('events'))
patientsToSort.sort(
sorter.byPropertiesOf(['-ActiveEventsCount', 'LastName'])
);
if (sortBy.includes('vae'))
patientsToSort.sort(sorter.byPropertiesOf(['-VaeStatus']));
console.log('patientsToSort---', patientsToSort);
setPatients(patientsToSort);
};
useEffect(() => {
if (!fetched) {
getPatientData();
}
}, []);
useEffect(() => {
console.log('setpatients called .. ', patients);
}, [patients]);
useEffect(() => {
const saved_sortby = localStorage.getItem('sortby');
if (saved_sortby) {
sortByCaseFn(saved_sortby, props.myPatientDetails);
} else sortByCaseFn('events', props.myPatientDetails);
setFetched(true);
}, [props.myPatientDetails]);
useEffect(() => {
sortByCaseFn(sortBy, patients);
}, [sortBy]);
return (
<> Render Patient List </> )
My Test Code :
Patients.test.tsx
jest.mock('react-redux', () => ({
useSelector: jest.fn(),
useDispatch: jest.fn()
}));
export const setHookTestState = (newState: any) => {
const setStateMockFn = () => {};
return Object.keys(newState).reduce((acc, val) => {
acc = acc?.mockImplementationOnce(() => [newState[val], setStateMockFn]);
return acc;
}, jest.fn());
};
describe('My Patient Screen', () => {
const useSelectorMock = reactRedux.useSelector as jest.Mock<any>;
const useDispatchMock = reactRedux.useDispatch as jest.Mock<any>;
beforeEach(() => {
useSelectorMock.mockImplementation((selector) => selector(mockStore));
useDispatchMock.mockImplementation(() => () => {});
});
afterEach(() => {
useDispatchMock.mockClear();
useSelectorMock.mockClear();
});
const mockInitialState = {
myPatientDetails: vaeMock,
fetching: false,
failedMsg: '',
requestPayload: {}
};
const mockStore = {
counter: undefined,
menu: undefined,
selectPatientProps: undefined,
myPatientProps: mockInitialState
};
test('validate sorting by events', async (done) => {
React.useState = setHookTestState({
patients: vaeMock,
sortBy: 'vae',
fetched: 'false'
});
const {
getByText,
getByRole,
getByTestId,
getAllByTestId,
findAllByTestId,
queryByText,
container
} = render(<Mypatient />);
await waitFor(() => {
expect(getByText('Ander, Sam')).toBeDefined();
});
const list = getAllByTestId('patientname');
expect(within(list[0]).getByText('Sara, Jone')).toBeInTheDocument(); //Fails here as Sorting doesnt happen
console.log('....list ', list);
});
});
My Observations:
The 'vaeMock' data that I set in redux state 'mockInitialState' is successfully sent as props
The 'vaeMock' data that I set in component state using setHookTestState is also set successfully.
The lifecycle events happens like this -
a. setPatients() is called using the component state data.
b. using props that is sent , sortByCaseFn is called but setPatients is not called.
c. again using the component state , sortByCaseFn is called but setPatients is not set.
Without setting the component state variables runs into a TypeError: Undefined is not iterable.
All Iam trying to do is - send a mockData to a component that uses useDispatch, useEffects
and sort the data on the component mount and initialize to local state variable.

mock api call in a custom hook using react test library

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.

React - Jest testing - TypeError: refetch is not a function

I'm using a useFocusEffect() hook in my component, which uses refetch from react-query. Code is working fine in the app. Issue I'm facing is with unit testing. I'm using Jest and getting an error that TypeError: refetch is not a function.
I've defined a mock function const refetchFunc = jest.fn(); and have provided it to refetch as a mockReturnValue, but still the error persist.
App.js:
const { data, refetch } = useStudents(
student?.id
);
useFocusEffect(
React.useCallback(() => {
refetch();
}, [refetch])
);
Jest Test:
import { Home } from './Home';
jest.mock('hooks/Auth/useLoadAuthStudentsData', () => ({
useLoadAuthStudentData: jest.fn()
}));
jest.mock('hooks/Student/useStudents', () => ({
useStudents: jest.fn()
}));
jest.mock('#react-navigation/core', () => {
return {
...jest.requireActual('#react-navigation/core'),
useNavigation: jest.fn(() => ({}))
};
});
const useLoadAuthStudentsData = userHook as ReturnType<typeof jest.fn>;
const useStudents = tasksHook as ReturnType<typeof jest.fn>;
const navContext = {
isFocused: () => true,
addListener: jest.fn(() => jest.fn())
};
const mockParams = {
params: {
studentId: 'studentId'
}
};
describe('Home', () => {
beforeEach(() => {
const refetchFunc = jest.fn();
useStudents.mockReturnValue({
data: Student.deserializeAsList(studentsStub),
isLoading: false,
isSuccess: true,
isError: false,
refetch:refetchFunc
});
useLoadAuthStudentData.mockReturnValue({ data: studentStub });
});
const component = (
<NavigationContext.Provider value={navContext}>
<Home route={mockParams} navigation={{ goBack: jest.fn() }} />
</NavigationContext.Provider>
);
it('should render app without error', () => {
expect(render(component)).toBeTruthy();
});
Error:
● Home › should render screen without error
TypeError: refetch is not a function
54 | useFocusEffect(
55 | React.useCallback(() => {
> 56 | refetch();
| ^
57 | }, [refetch])
58 | );
59 |
Have you tried with this for mocking your student hook :
jest.mock('hooks/Student/useStudents', () => {
const data = {};
const refetch = jest.fn()
return{
useStudents: jest.fn(() => return{ data:data,refetch:refetch })
}
,
});
Do lemme know if this works

Jest - destructure property

export const AppLayout: React.FunctionComponent = React.memo(({ children }) => {
// Application main layout component name
AppLayout.displayName = getComponentName('App-Layout');
const { isAuthenticated } = useAuth();
const { sendRequest } = useApiService();
React.useEffect(() => {
const fetchData = async () => {
try {
...
} catch (err) {
console.error(err);
}
};
isAuthenticated() && fetchData();
}, []);
describe('App General component', () => {
const useAuth = jest.fn();
const useApiService = jest.fn();
const isAuthenticated = true;
const props = {};
const renderComponent = () => render(
<AppLayout/>
);
it('should render without errors', () => {
renderComponent();
});
/**
* Validate current user exist in session
* #returns {boolean}
*/
const isAuthenticated = React.useCallback((): boolean => {
return Boolean(user);
}, [user]);
How can I set isAuthenticated to true so I can avoid the error
TypeError: Cannot destructure property 'isAuthenticated' of
const mockUseAuthIsAuthenticated = jest.fn(() => false);
const mockUseAuth = jest.fn(() => ({
isAuthenticated: mockUseAuthIsAuthenticated,
});
jest.mock("../hooks/useAuth", mockUseAuth);
describe('My test case', () => {
it(`should return authenticated=TRUE`, () => {
// Given
mockUseAuthIsAuthenticated.mockImplementationOnce(
() => true
);
// When
// assuming `render` comes from the react testing-library
render(<ComponentThatCallsTheHook />);
// Then
expect(mockUseAuthIsAuthenticated).toHaveBeenCalledOnce();
// ... more expectations
});
});
You should mock the useAuth hook like this:
jest.mock("yourUseAuthPath", () => ({
useAuth: () => ({
isAuthenticated: () => true
}),
}));
describe('App General component', () => {
...
}
n.b. You should replace the yourUseAuthPath with the correct path where you get the useAuth from. Example:
import { useAuth } from "yourUseAuthPath";
Some official docs here: https://jestjs.io/docs/mock-functions#mocking-partials

How to mock React stateless currying function using Jest & Enzyme

I have this stateless React component:
...
const Providers = ({ onSelectFeedProvider, ... }) => {
const handleSelectFeedProvider = value => e => {
e.preventDefault();
onSelectFeedProvider({ target: { value } });
};
return {
<Row onClick={handleSelectFeedProvider(1)}>
...
</Row>
}
}
And the test:
import Row from 'components/Common/Row';
import Providers from './index';
jest.mock('components/Common/Row', () => 'Row');
let onSelectFeedProviderSpy = jest.fn();
let onSelectProviderSpy = jest.fn();
const initialProps = {
feedProvider: 0,
onSelectFeedProvider: () => onSelectFeedProviderSpy(),
selectedProvider: undefined,
onSelectProvider: () => onSelectProviderSpy()
};
const mockComponent = props => {
const finalProps = { ...initialProps, ...props };
return <Providers {...finalProps} />;
};
it('should call correctly', () => {
const wrapper = shallow(mockComponent());
wrapper.find(Row).simulate('click', 'what do I have to do here');
expect(onSelect).toHaveBeenCalledTimes(1);
});
How can I do to call the method correctly and pass the coverage? I think have tried all the possibilities. Any thoughts?
You don't have many options in this, one approach is to have onSelect injectable
const Component = ({onSelect}) => {
const handleSelect = value => e => {
e.preventDefault()
onSelect && onSelect({ target: { value } })
}
return <Row onClick={handleSelect(1)} />
}
Test
it('should call correctly', () => {
const spy = jest.fn()
const wrapper = shallow(mockComponent({onSelectProvider: spy}));
wrapper.find(Row).simulate('click', 'what do I have to do here');
expect(spy).toHaveBeenCalledTimes(1);
});

Resources