How to Test React Hooks useEffect, useCallBack
React internals shouldn't be mocked unless necessary because this results in synthetic tests that don't conform to the way the framework works and give false positives. This especially applies to hooks like useEffect
because they have hidden state and may not work as a tester expects.
React functional components don't expose component instance and supposed to be tested by asserting the result. Tests can be strengthened up with spy assertions to make results less ambiguous.
Since listeners are set on document
, it needs a focus, something like:
jest.spyOn(document, 'addEventListener');
jest.spyOn(document, 'removeEventListener');
const onCloseSpy = jest.fn();
const component = mount(<ModalComponent closeModal={onCloseSpy} />);
expect(component.find(Header).prop('onClose')).toBe(onCloseSpy);
expect(document.addEventListener).toBeCalledTimes(1);
expect(document.addEventListener).toBeCalledWith('keydown', expect.any(Function));
document.dispatchEvent(new KeyboardEvent('keydown', {keyCode: 37}));
expect(onCloseSpy).not.toBeCalled();
document.dispatchEvent(new KeyboardEvent('keydown', {keyCode: 27}));
expect(onCloseSpy).toBeCalledWith(false);
// rerender to make sure listeners are set once
component.setProps({});
expect(document.addEventListener).toBeCalledTimes(1);
expect(document.removeEventListener).not.toBeCalled();
// unmount
component.unmount();
expect(document.removeEventListener).toBeCalledTimes(1);
const [, callback] = document.addEventListener.mock.calls[0];
expect(document.removeEventListener).toBeCalledWith('keydown', callback);
Related videos on Youtube
Kumar Pranay
Updated on June 04, 2022Comments
-
Kumar Pranay almost 2 years
I'm trying to write unit test cases using Jest, Enzyme for useEffect, and useCallback for React hooks but I'm unable to succeed. Can you someone help me to write a test case for the below code.
ModalComponent.jsx
const ModalComponent = ({ closeModal }) => { const handleModal = useCallback((event) => { if (event.keyCode === 27) { closeModal(false); } } useEffect(() => { document.addEventListener('keydown', handleModal); return () => document.removeEventListener('keydown', handleModal); }, []); return ( <Modal> <Header onClose={closeModal} /> <Body /> <Footer /> </Modal> ); }
ModalComponent.spec.jsx
describe('Modal Component', () => { let props; beforeEach(() => { props = { closeModal: jest.fn(), }; }; it('should handle useEffect', () => { jest.spyOn(React, 'useEffect').mockImplementation(f => f()); document.addEventListener('keydown', handleModal); document.removeEventListener('keydown', handleModal); const component = shallow(<ModalComponent />); }); });
It is unable to cover these lines
document.addEventListener('keydown', handleModal);
,document.removeEventListener('keydown', handleModal);
,if(event.keyCode === 27)
,closeModal(false)
. How can I cover the test cases?-
Estus Flask almost 4 yearsHooks weren't designed to test the implementation. Test the behaviour.
-
Estus Flask almost 4 yearsPossible duplicate of stackoverflow.com/questions/38960832/…
-
Kumar Pranay almost 4 years@EstusFlask, I was able to get this test case covered by mocking useEffect and useCallback.
jest.spyOn(React, 'useEffect').mockImplementation(f => f());
-
Estus Flask almost 4 yearsIt's possible but this is not how it's usually done. You shouldn't mock the framework itself without a good reason, this may result in purely synthetic tests that don't meet real-world expectations. This especially applies to hooks. A correct way to test this is to trigger
keydown
event, or at least spy/mockdocument
methods and assert their calls. -
Kumar Pranay almost 4 years@EstusFlask how do I do that. If you can provide an example that will be really helpful. I referred to your link mentioned in the above comment they finding the element and simulating the event on that like this
wrapper.find('input').simulate('keypress', {key: 'Enter'})
but in my case, I cannot pass input or any other element tofind
method right so could you guide me to get this work? -
Estus Flask almost 4 yearsIt's
document
that listens so it should be triggered there. Enzyme's simulate works only on React listeners, not raw DOM. See stackoverflow.com/questions/33638385/… , I suppose that's your case. -
Kumar Pranay almost 4 years@EstusFlask, Thank you for that. How do I use that
dispatchEvent
when a component Unmounts. I meanReact.useEffect()
will Unmoun as well when we return the function from it like thisReact.useEffect(() => { document.addEventListener('keydown', handleModal); return () => document.removeEventListener('keydown', handleModal) }, [])
right. So, I written my test like this. -
Kumar Pranay almost 4 years
it('should handle useEffect', () => { jest.spyOn(React, 'useEffect').mockImplementation(f => f()); const event = new KeyboardEvent('keydown', {'key': 'Escape'}); document.dispatchEvent(event); const component = shallow(<ModalComponent />); expect(component).toMatchSnapshot(); });
-
Estus Flask almost 4 yearsYou shouldn't mock useEffect when doing this, this prevents the component from working normally. It's either one, or another. I posted how it should look like but can't test it now. Also, I'd suggest to stick to
keyCode
in tests, I don't expect Jest DOM implementation to be that smart to translatekey
tokeyCode
.
-
-
Kumar Pranay almost 4 yearsI tried this test cases in my machine. The below line is failing and however, it is unable to cover useEffect() at all.
expect(document.addEventListener).toBeCalledTimes(1);
-
Estus Flask almost 4 yearsI see. There's ongoing problem with
shallow
that requires to patch useEffect similarly to how you did, github.com/enzymejs/enzyme/issues/2086 , which is a big turn-off. I'd suggest to use deep rendering withmount
instead at this moment, you can stub nested components in case if you don't want them to interfere with the test. Alternatively, try github.com/mikeborozdin/jest-react-hooks-shallow