How to Test React Hooks useEffect, useCallBack

12,039

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);
Share:
12,039

Related videos on Youtube

Kumar Pranay
Author by

Kumar Pranay

Updated on June 04, 2022

Comments

  • Kumar Pranay
    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
      Estus Flask almost 4 years
      Hooks weren't designed to test the implementation. Test the behaviour.
    • Estus Flask
      Estus Flask almost 4 years
    • Kumar Pranay
      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
      Estus Flask almost 4 years
      It'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/mock document methods and assert their calls.
    • Kumar Pranay
      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 to find method right so could you guide me to get this work?
    • Estus Flask
      Estus Flask almost 4 years
      It'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
      Kumar Pranay almost 4 years
      @EstusFlask, Thank you for that. How do I use that dispatchEvent when a component Unmounts. I mean React.useEffect() will Unmoun as well when we return the function from it like this React.useEffect(() => { document.addEventListener('keydown', handleModal); return () => document.removeEventListener('keydown', handleModal) }, []) right. So, I written my test like this.
    • Kumar Pranay
      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
      Estus Flask almost 4 years
      You 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 translate key to keyCode.
  • Kumar Pranay
    Kumar Pranay almost 4 years
    I 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
    Estus Flask almost 4 years
    I 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 with mount 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