Given a custom hook that looks like this:
const getSavedInfo (id) => {
const endpoint = getEndpoint(id)
const {updateInfo} = React.useContext(infoContext)
axios.get(endpoint).then((res) => {
if (res) {
updateInfo(res.data)
}
})
}
How would I go about properly mocking the updateInfo method so I can make sure it was called within my jest test?
You need to pass the provider in test while rendering the custom hook. You can write your own provider function or use react-hooks-testing-library which does the same thing for you. Like this:
import { renderHook } from 'react-hooks-testing-library';
const wrapper = ({ children }) => (
<InfoContext.Provider value={{updateInfo: jest.fn}}>{children}</InfoContext.Provider>
);
it('should call update info', () => {
const { result } = renderHook(() => getSavedInfo(), { wrapper });
// rest of the test code goes here
});
You can find detailed explanation here
Related
I am using a custom hook from 3rd party library in my React project:
import { useProductData } from '#third/prod-data-component';
const ProductRow: React.FC<MyProduct> = ({ product }) => {
// using the custom hook here
const productData = useProductData();
})
The signature of that hook function is:
export declare const useProductData: () => string | undefined;
In my jest test, I would like to mock the returned value of the hook, I tried:
it('should show correct product data', ()=>{
jest.mock('#third/prod-data-component', () => {
return { useProductData: jest.fn(()=>'foo')}
});
...
...
})
When I run test, the above mock doesn't take any effect.
How to mock the return value of custom hook that is from a 3rd party library?
==== UPDATE ====
I also tried this:
jest.mock('#third/prod-data-component', () => {
const lib = jest.requireActual('#third/prod-data-component');
return {...lib, useProductData: () => 'foo'}
});
But does't work either.
can you try this
import {useProductData} from '#third/prod-data-component'
jest.mock('#third/prod-data-component');
(useProductData as jest.Mock).mockImplementation(() => {mockKey: 'mockData'})
describe('test scenario', () => {
it('should show correct product data', () => {
// your assertions
})
})
Reading your example, you may be missing the actual module import.
However, have you tried the full module mocking?
import moduleMock from '#third/prod-data-component'
jest.mock('#third/prod-data-component')
describe('test scenario', () => {
it('should show correct product data', () => {
moduleMock.useProductData.mockReturnValue(...)
})
})
EDIT
I missed the Typescript part. You need to wrap the actual module type definitions with Jest's own type definitions.
I solved this problem in the past in the following way, using jest.mocked function:
import prod_data_component_module from '#third/prod-data-component'
jest.mock('#third/prod-data-component')
describe('Your test case scenario', () => {
const mock = jest.mocked(prod_data_component_module, { shallow: true })
it('Your test here', async () => {
mock.useProductData.mockReturnValue('...')
// test logic here
})
})
mock is a jest wrapper of the original module. It is of type jest.MockedFn<T>, so it contains actual exported types/functions and also jest mocks.
how would you test a hook that is using navigator.geolocation.getCurrentPosition() method (mocked it already based on this: https://newdevzone.com/posts/how-to-mock-navigatorgeolocation-in-a-react-jest-test) in useEffect?
I can't see the way how to wait for result as upon calling the function it does not return anything and returned data are processed in passed callback. found some hackish solution that works when run debug using jest runner vsc extension, but does not for regular test run.
the hook:
const useGeolocation = () => {
const [coords, setCoords] = useState<GeolocationCoordinates>();
useEffect(() => {
navigator.geolocation.getCurrentPosition((position) => {
setCoords(position.coords);
});
}, []);
return coords;
};
test:
import { renderHook } from '#testing-library/react';
it('should return non-empty coords', async () => {
const { result } = renderHook(() => useGeolocation());
// MISSING CODE
expect(result.current).not.toBe(undefined);
});
I'll be happy for any idea leading to solution so I don't need to give up on unit testing once again :slight_smile:
I'm writing unit test for once of my .ts file. Where I'm facing a problem and unable to find the solution. Hopefully someone can help me to resolve it.
Problem
While writing unit test. I'm unable to test the value for profile. After calling a method called getProfile().
File setup
Profile.ts
import { getProfileAPI} from "./api";
class ProfileDetails implements IProfileDetails {
public profile: string = ''
constructor() {}
getProfile = async () => {
const { data } = await getProfileAPI();
if (data) {
this.profile = data
}
};
}
const profileDetail = new ProfileDetails();
export default profileDetail;
Profile.spec.ts
import Profile from './Profile';
describe('Profile', () => {
it('getProfile', async () => {
Profile.getProfile = jest.fn();
await Profile.getProfile();
expect(Profile.getProfile).toHaveBeenCalled();
});
});
So the challenge I'm facing here is, I can able to mock the getProfile method. But I'm not able to mock the getProfileAPI function which is called inside the getProfile method.
How can I mock a function which is called inside a mocked method (or) is there any other way to resolve this. Kindly help.
Thanks in advance.
Before answering your questions, I may have some comments :
your test is wrong, all it does is calling the method then checking if it is called, of course it will always pass !
you are not really mocking, in fact you're erasing the old method and it may have some impacts on other tests.
your method "getProfile" should be called "getAndSetProfile", or "syncProfile", or something like that, getProfile is confusing for a developer, he will think it only get the profile and returns it.
I don't recommend creating & exporting an instance of ProfileDetails like this, you should take a look on DI (Dependency Injection) with typedi for example.
Do not forget :
A unit test means that any dependency inside your "unit" should be mock, you must only test the logic inside your "unit" (in your case, the getProfile function, or the class itself).
Here, you are invoking a method called "getProfileAPI" from another service that is not mocked, so you are currently testing its logic too.
This test should work :
Profile.spec.ts
jest.mock('./api', () => ({
getProfileAPI: jest.fn(),
}));
import { getProfileAPI } from "./api";
import Profile from './Profile';
describe('Profile', () => {
it('getProfile', async () => {
await Profile.getProfile();
expect(getProfileAPI).toHaveBeenCalled();
});
});
In our example, Profile.profile will be empty, because even if we mocked to getProfileAPI method, we didn't make it return something. You could test both cases :
jest.mock('./api', () => ({
getProfileAPI: jest.fn(),
}));
import { getProfileAPI } from "./api";
import Profile from './Profile';
const mockGetProfileAPI = getProfileAPI as jest.Mock; // Typescript fix for mocks, else mockResolvedValue method will show an error
describe('Profile', () => {
describe('getProfile', () => {
describe('with data', () => {
const profile = 'TEST_PROFILE';
beforeEach(() => {
mockGetProfileAPI.mockResolvedValue({
data: profile,
});
});
it('should call getProfileAPI method', async () => {
await Profile.getProfile();
expect(mockGetProfileAPI).toHaveBeenCalled(); // Please note that "expect(getProfileAPI).toHaveBeenCalled();" would work
});
it('should set profile', async () => {
await Profile.getProfile();
expect(Profile.profile).toBe(profile);
});
});
describe.skip('with no data', () => {
it('should not set profile', async () => {
await Profile.getProfile();
expect(Profile.profile).toStrictEqual(''); // the default value
});
});
});
});
NB : I skipped the last test because it won't work in your case. Profile isn't recreated between tests, and as it is an object, it keeps the value of Profile.profile (btw, this is a bit weird) between each tests. This is one of the reasons why you should not export a new instance of the class.
I'm trying to write a unit test to check that a function (passed as a prop) gets called if another prop is true in useEffect hook. The unit test fails to confirm that the (mocked) function is called in the useEffect hook, but it can confirm that a function that is spyOn from an imported module is called. Does anyone know what might be the issue? Thanks!
import {getUser} from './Auth';
export function ComponentA({
shouldRetryExport,
someReduxDispatchFunc,
}) {
const handleExport = useCallback(async () => {
const user = await getUser();
someReduxDispatchFunc();
}, []);
useEffect(() => {
if (shouldRetryExport) {
handleExport();
}
}, [shouldRetryExport]);
return (<SomeComponent />)
});
Unit test:
import * as Auth from './Auth';
it('should call someReduxDispatchFunc if getUserAuthorization is true', () => {
const getAuthUserSpy = jest.spyOn(Auth, 'getUser');
const someReduxDispatchFuncMock = jest.fn();
const props = {
someReduxDispatchFunc: someReduxDispatchFuncMock,
shouldRetryExportWithUserReAuthorization: true,
};
enzyme.mount(<ComponentA {...props} />);
expect(getAuthUserSpy).toHaveBeenCalled(); // works -> returns true
expect(someReduxDispatchFuncMock).toHaveBeenCalled(); // doesn't work -> returns false
});
It seems like it has something to do with the useCallback with useEffect. If I remove the useCallback and add the logic within to useEffect, it can capture someReduxDispatchFuncMock has been called.
I don't think the problem is from either useCallback or useEffect. The problem is most likely your callback takes an async function which means it needs time to get resolved.
In order to this, you have to make your test as async then wait it to get resolved as following:
it('should call someReduxDispatchFunc if getUserAuthorization is true', async () => {
const getAuthUserSpy = jest.spyOn(Auth, 'getUser');
const someReduxDispatchFuncMock = jest.fn();
const props = {
someReduxDispatchFunc: someReduxDispatchFuncMock,
shouldRetryExport: true,
};
enzyme.mount(<ComponentA {...props} />);
// wait for getting resolved
await Promise.resolve();
expect(getAuthUserSpy).toHaveBeenCalled();
expect(someReduxDispatchFuncMock).toHaveBeenCalled();
});
I am trying to write the Jest-enzyme test case for useEffect react hooks, and I am really lost, I want to write test case for 2 react hooks, one making the async call and another sorting the data and setting the data using usestate hooks, my file is here.
export const DatasetTable: React.FC<DatasetTableProps> = ({id, dataset, setDataset, datasetError, setDataSetError}) => {
const [sortedDataset, setSortedDataset] = useState<Dataset[]>();
useEffect(() => {
fetchRegistryFunction({
route:`/dataset/report?${constructQueryParams({id})}`,
setData: setDataset,
setError: setDataSetError
})();
}, [id, setDataset, setDataSetError]});
useEffect(() => {
if(dataset) {
const sortedDatasetVal = [...dataset];
sortedDatasetVal.sort(a, b) => {
const dateA: any = new Date(a.date);
const dateA: any = new Date(a.date);
return dataA - dateB;
}
setSortedDataset(sortedDatasetVal);
}
}, [dataset])
return (
<div>
<DatasetTable
origin="Datasets"
tableData={sortedDataset}
displayColumns={datasetColumns}
errorMessage={datasetError}
/>
</div>
);
}
Enzyme isn't the right library for this kind of testing.
https://react-hooks-testing-library.com/ is what you need.
In your case I would extract all the data fetching to a 'custom hook' and then test this independently from your UI presentation layer.
In doing so you have better separation of concerns and your custom hook can be used in other similar react components.
I managed to get enzyme to work with a data fetching useEffect hook. It does however require that you allow your dataFetching functions to be passed as props to the component.
Here's how I would go about testing your component, considering it now accepts fetchRegistryFunction as a prop:
const someDataSet = DataSet[] // mock your response object here.
describe('DatasetTable', () => {
let fetchRegistryFunction;
let wrapper;
beforeEach(async () => {
fetchRegistryFunction = jest.fn()
.mockImplementation(() => Promise.resolve(someDataSet));
await act(async () => {
wrapper = mount(
<DatasetTable
fetchRegistryFunction={fetchRegistryFunction}
// ... other props here
/>,
);
});
// The wrapper.update call changes everything,
// act seems to not automatically update the wrapper,
// which lets you validate your old rendered state
// before updating it.
wrapper.update();
});
afterEach(() => {
wrapper.unmount();
jest.restoreAllMocks();
});
it('should display fetched data', () => {
expect(wrapper.find(DatasetTable).props().tableData)
.toEqual(someDataSet);
});
});
Hope this helps!