Testing a custom hook not getting updated - reactjs

Trying to test a status hook that uses a promise that is not getting updated by my test.
screens.OnStart() should trigger setStatus with the value the promise returns.
When I log status it never changes.
import { useEffect, useState } from 'react'
import screens from '#utils/screen'
const useStatus = () => {
const [status, setStatus] = useState()
useEffect(() => {
const listener = screens.OnStart(
"HAPPEN",
({ status }) =>
setStatus(status)
)
return () => {
screens.removeListener("HAPPEN", listener)
}
}, [])
return {
status,
}
}
export default useStatus
Test
import React from 'react'
import { act, renderHook } from '#testing-library/react-hooks'
import useStatus from '#hooks/useStatus'
const mockedOnStart = jest.fn().mockImplementation((event, callback) => callback)
jest.mock('#utils/screens', () => ({
...jest.requireActual('#utils/screens'),
default: {
OnStart: () => mockedOnStart(),
},
__esModule: true,
}))
describe('useStatus', () => {
test('Renders', async () => {
mockedOnStart.mockReturnValueOnce(2)
const { result } = renderHook(() => useStatus())
await act(async () => {
console.log('result = ', result.current.status)
})
})
})

Related

How to mock a custom async React hook including useRef() with jest and react-testing-library to access result.current in Typescript

I have a custom hook which includes a reference. How do I properly test such a hook and how do I mock the useRef()?
const useCustomHook = (
ref: () => React.RefObject<Iref>
): {
initializedRef: boolean
} => {
const [initializedRef, setInitializedRef] = useState<boolean>(false)
useEffect(() => {
if (ref) {
const { current } = ref()
// here comes your ref code
console.log({ refCurrent: current })
}
setInitializedRef(true)
}, [ref])
return { initializedRef }
}
Here are two possibilities:
view solution
import React, { useEffect, useRef, useState } from 'react'
import { renderHook, RenderHookResult, waitFor } from '#testing-library/react'
interface Iref {
current: any
}
const useCustomHookView = (
view: RenderHookResult<React.RefObject<Iref>, unknown>
): {
initializedView: boolean
} => {
const [initializedView, setInitializedView] = useState<boolean>(false)
useEffect(() => {
if (view && view.result) {
console.log({ viewResult: view.result.current })
}
setInitializedView(true)
}, [view])
return { initializedView }
}
describe('useRefMock', () => {
beforeEach(() => {
jest.clearAllMocks()
})
it('view', async () => {
const view = renderHook(() =>
useRef<Iref>({ current: 'my fake' })
)
const { result, rerender } = renderHook(() => useCustomHookView(view))
rerender()
expect(result.current.initializedView).toBeTruthy()
})
})
ref solution
import React, { useEffect, useRef, useState } from 'react'
import { renderHook, RenderHookResult, waitFor } from '#testing-library/react'
interface Iref {
current: any
}
const useCustomHookRef = (
ref: () => React.RefObject<Iref>
): {
initializedRef: boolean
} => {
const [initializedRef, setInitializedRef] = useState<boolean>(false)
useEffect(() => {
if (ref) {
const { current } = ref()
console.log({ refCurrent: current })
}
setInitializedRef(true)
}, [ref])
return { initializedRef }
}
describe('useRefMock', () => {
beforeEach(() => {
jest.clearAllMocks()
})
it('ref', async () => {
const ref = (): React.RefObject<Iref> => {
const { result } = renderHook(() => useRef<Iref>({ current: 'my fake' }))
console.log({ resultCurrent: result.current })
return result
}
const { result, rerender } = renderHook(() => useCustomHookRef(ref))
rerender()
expect(result.current.initializedRef).toBeTruthy()
})

How can I test the custom fetch hook with dummy data?

I have a custom fetch hook. It is not doing a real fetch process.
It's an implantation example. I want to write a proper test for it.
This is the pseudo API.
import { data } from './data';
import { IProduct } from '../common/types';
// pseudo API
export const getProducts = (): Promise<IProduct[]> => {
return new Promise((resolve, reject) => {
if (!data) {
return setTimeout(() => reject(new Error('data not found')), 1000);
}
setTimeout(() => resolve(data), 1000);
});
};
Here is the useFetch.ts
import { useEffect, useState } from 'react';
import { getProducts } from '../api/api';
import { IProduct } from '../common/types';
export const useFetch = () => {
const [products, setProducts] = useState<IProduct[]>([]);
const [categories, setCategories] = useState<string[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<unknown>(null);
useEffect(() => {
const fetchProducts = async () => {
setLoading(true);
try {
const res = await getProducts();
const categories = Array.from(new Set(res.map((r) => r.category)));
setProducts(res);
setCategories(categories);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
fetchProducts();
}, []);
return { products, categories, loading, error };
};
Here is the useFetch.test.ts
import { renderHook } from '#testing-library/react-hooks';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { data } from '../api/data';
import { useFetch } from './useFetch';
const server = setupServer(
rest.get('/api', (req, res, ctx) => {
return res(ctx.json(data));
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
test('gets the data', async () => {
const { result, waitForNextUpdate } = renderHook(() => useFetch());
await waitForNextUpdate();
expect(result.current).toEqual(data);
});
How can I write a proper test for this case?
I'm getting the received, expected error.
expect(received).toEqual(expected) // deep equality

Write the test case for useEffect with Fetch API and useState in react

Following are my code which includes the fetch API(getData) call with the useEffect and once get the response it will set the result into the setData using useState
I am trying to write the test case for the useEffect and useState but its failing and when I am seeing into the coverage ,I am getting the red background color with statements not covered for the useEffect block.
import { getData } from '../../api/data';
const [data, setData] = useState({});
useEffect(() => {
getData({ tableName }).then((response) => {
try {
if (response && response.result) {
const result = Array.isArray(response.result)
? response.result[0]
: response.result;
const createDate = result.createdDate;
result.name = result.firstName;
result.submittedDate = `${createDate}`;
result.attribute = Array.isArray(result.attribute)
? result.attribute
: JSON.parse(result.attribute);
setData(result);
}
} catch (error) {
const errorObj = { error: error.message || 'error' };
setData({ errorObj });
}
});
}, []);
And I tried to write the test cases as following for the above code.
import React from "react";
import {
shallowWithIntl,
loadTranslation,
} from "../../../node_modules/enzyme-react-intl/lib/enzyme-react-intl";
import ParentPage from "ParentPage";
import ChildPage from "ChildPage";
import mockResponse from "mockData";
import { shallow, mount } from "enzyme";
import { act } from "react-dom/test-utils";
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve(mockResponse),
})
);
describe("ParentPage", () => {
let useEffect;
let wrapper;
const mockUseEffect = () => {
useEffect.mockImplementationOnce((f) => f());
};
beforeEach(() => {
const defaultProps = {
tableName: "tableName",
};
wrapper = shallowWithIntl(<ParentPage {...defaultProps} />);
useEffect = jest.spyOn(React, "useEffect");
mockUseEffect();
});
it("Should render", () => {
expect(wrapper).toMatchSnapshot();
});
it("Compenent render", async () => {
let wrapper;
await act(async () => {
const setWidgets = jest.fn();
const useStateSpy = jest.spyOn(React, "useState");
useStateSpy.mockImplementation([mockResponse, setWidgets]);
wrapper = await mount(<ChildPage data={mockResponse} />);
await act(async () => {
wrapper.update();
});
console.log(wrapper);
});
});
});
But when I tried using npm run test,And check the coverage I am still getting the statements not covered for the useEffect and useState.
What should I do to achieve the coverage as maximum as possible?

how to test a hook with async state update in useEffect?

i have a simple hook that fetches the value and sets it to option as follows:
import Fuse from 'fuse.js'
import React from 'react'
// prefetches options and uses fuzzy search to search on that option
// instead of fetching on each keystroke
export function usePrefetchedOptions<T extends {}>(fetcher: () => Promise<T[]>) {
const [options, setOptions] = React.useState<T[]>([])
React.useEffect(() => {
// fetch options initially
const optionsFetcher = async () => {
try {
const data = await fetcher()
setOptions(data)
} catch (err) {
errorSnack(err)
}
}
optionsFetcher()
}, [])
// const fuseOptions = {
// isCaseSensitive: false,
// keys: ['name'],
// }
// const fuse = new Fuse(options, fuseOptions)
// const dataServiceProxy = (options) => (pattern: string) => {
// // console.error('options inside proxy call', { options })
// const optionsFromSearch = fuse.search(pattern).map((fuzzyResult) => fuzzyResult.item)
// return new Promise((resolve) => resolve(pattern === '' ? options : optionsFromSearch))
// }
return options
}
i am trying to test it with following code:
import { act, renderHook, waitFor } from '#testing-library/react-hooks'
import { Wrappers } from './test-utils'
import { usePrefetchedOptions } from './usePrefetchedOptions'
import React from 'react'
const setup = ({ fetcher }) => {
const {
result: { current },
waitForNextUpdate,
...rest
} = renderHook(() => usePrefetchedOptions(fetcher), { wrapper: Wrappers })
return { current, waitForNextUpdate, ...rest }
}
describe('usePrefetchedOptions', () => {
const mockOptions = [
{
value: 'value1',
text: 'Value one',
},
{
value: 'value2',
text: 'Value two',
},
{
value: 'value3',
text: 'Value three',
},
]
test('searches for appropriate option', async () => {
const fetcher = jest.fn(() => new Promise((resolve) => resolve(mockOptions)))
const { rerender, current: options, waitForNextUpdate } = setup({ fetcher })
await waitFor(() => {
expect(fetcher).toHaveBeenCalled()
})
// async waitForNextUpdate()
expect(options).toHaveLength(3) // returns initial value of empty options = []
})
})
the problem is when i am trying to assert the options at the end of the test, it still has the initial value of []. However if I log the value inside the hook, it returns the mockOptions. How do I update the hook after it is update by useEffect but in async manner.
I have also tried using using waitForNextUpdate where it is commented in the code. it times out with following error:
Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.Error:
Couple things, currently you're waiting for fetcher to be called in your tests, but the state update actually happens not after fetcher is called but after the promise that fetcher returns is resolved. So you'd need to wait on the resolution of that promise in your test
Also, you're destructuring the value of result.current when you first render your hook. That value is just a copy of result.current after that first render and it will not update after that. To query the current value of options, you should query result.current in your assertion instead.
const fetcherPromise = Promise.resolve(mockOptions);
const fetch = jest.fn(() => fetcherPromise);
const { result } = renderHook(() => usePrefetchedOptions(fetcher), { wrappers: Wrappers })
await act(() => fetcherPromise);
expect(result.current).toHaveLength(3)
Here's what worked for me whenI needed to test the second effect of my context below:
import React, {createContext, useContext, useEffect, useState} from "react";
import {IGlobalContext} from "../models";
import {fetchGravatar} from "../services";
import {fetchTokens, Token} from "#mylib/utils";
const GlobalContext = createContext<IGlobalContext>({} as IGlobalContext);
function useGlobalProvider(): IGlobalContext {
const [token, setToken] = useState<Token>(Token.deserialize(undefined));
const [gravatar, setGravatar] = useState<string>('');
useEffect(() => {
setToken(fetchTokens());
}, []);
useEffect(() => {
if (token?.getIdToken()?.getUsername()) {
fetchGravatar(token.getIdToken().getUsername())
.then(setGravatar)
}
}, [token]);
const getToken = (): Token => token;
const getGravatar = (): string => gravatar;
return {
getToken,
getGravatar
}
}
const GlobalProvider: React.FC = ({children}) => {
const globalContextData: IGlobalContext = useGlobalProvider();
return (
<GlobalContext.Provider value={globalContextData}>{children}</GlobalContext.Provider>
);
};
function useGlobalContext() {
if (!useContext(GlobalContext)) {
throw new Error('GlobalContext must be used within a Provider');
}
return useContext<IGlobalContext>(GlobalContext);
}
export {GlobalProvider, useGlobalContext};
corresponding tests:
import React from "react";
import {GlobalProvider, useGlobalContext} from './Global';
import {act, renderHook} from "#testing-library/react-hooks";
import utils, {IdToken, Token} from "#mylib/utils";
import {getRandomGravatar, getRandomToken} from 'mock/Token';
import * as myService from './services/myService';
import {Builder} from "builder-pattern";
import faker from "faker";
jest.mock('#mylib/utils', () => ({
...jest.requireActual('#mylib/utils')
}));
describe("GlobalContext", () => {
it("should set Token when context loads", () => {
const expectedToken = getRandomToken('mytoken');
const spyFetchToken = spyOn(utils, 'fetchTokens').and.returnValue(expectedToken);
const wrapper = ({children}: { children?: React.ReactNode }) => <GlobalProvider>{children} </GlobalProvider>;
const {result} = renderHook(() => useGlobalContext(), {wrapper});
expect(spyFetchToken).toHaveBeenCalled();
expect(result.current.getToken()).toEqual(expectedToken);
})
it("should fetch Gravatar When Token username changes", async () => {
const expectedToken = getRandomToken('mytoken');
const expectedGravatar = getRandomGravatar();
const returnedGravatarPromise = Promise.resolve(expectedGravatar);
const spyFetchToken = spyOn(utils, 'fetchTokens').and.returnValue(expectedToken);
const spyFetchGravatar = spyOn(myService, 'fetchGravatar').and.returnValue(returnedGravatarPromise);
const wrapper = ({children}: { children?: React.ReactNode }) =>
<GlobalProvider>{children} </GlobalProvider>;
const {result, waitForValueToChange} = renderHook(() => useGlobalContext(), {wrapper});
// see here
// we need to wait for the promise to be resolved, even though the gravatar spy returned it
let resolvedGravatarPromise;
act(() => {
resolvedGravatarPromise = returnedGravatarPromise;
})
await waitForValueToChange(() => result.current.getGravatar());
expect(spyFetchToken).toHaveBeenCalled();
expect(result.current.getToken()).toEqual(expectedToken);
expect(spyFetchGravatar).toHaveBeenCalledWith(expectedToken.getIdToken().getUsername());
expect(resolvedGravatarPromise).toBeInstanceOf(Promise);
expect(result.current.getGravatar()).toEqual(expectedGravatar);
})
})

How to spyOn to react custom hook returned value?

Here is my custom hook:
useCustomModal.ts
export const useCustomModal = (modalType: string) => {
const setModal = () => {
// ... functionality here
};
const handleModalClose = (modalType: string) => {
// ... functionality here
setModal();
// ...
};
return {
handleModalClose,
setModal,
};
};
And here is my test:
useCustomModal.ts
import { act } from '#testing-library/react-hooks';
import { useCustomModal } from './useCustomModal';
describe('some', () => {
it('a test', async () => {
await act(async () => {
const actions = useCustomModal('test');
const spy = jest.spyOn(actions, 'setModal');
actions.handleModalClose('test');
expect(spy).toBeCalledTimes(1);
});
});
});
Test failed :
Expected number of calls: 1
Received number of calls: 0
How to properly spyOn on custom react hooks?
You need to use renderHook in conjunction with act. Something like this:
import { renderHook, act } from '#testing-library/react-hooks';
import { useCustomModal } from './useCustomModal';
describe('some', () => {
it('a test', () => {
const { result } = renderHook(() => useCustomModal('test'));
const spy = jest.spyOn(result.current, 'setModal');
act(() => {
result.current.handleModalClose('test');
});
expect(spy).toBeCalledTimes(1);
});
});

Resources