3

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?

Kumar Pranay
  • 53
  • 1
  • 1
  • 4
  • 1
    Hooks weren't designed to test the implementation. Test the behaviour. – Estus Flask Jul 12 '20 at 05:40
  • Possible duplicate of https://stackoverflow.com/questions/38960832/how-do-you-simulate-an-keydown-enter-event-or-others-in-enzyme – Estus Flask Jul 12 '20 at 05:41
  • @EstusFlask, I was able to get this test case covered by mocking useEffect and useCallback. `jest.spyOn(React, 'useEffect').mockImplementation(f => f());` – Kumar Pranay Jul 15 '20 at 17:14
  • 1
    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. – Estus Flask Jul 15 '20 at 17:58
  • @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? – Kumar Pranay Jul 16 '20 at 03:59
  • It's `document` that listens so it should be triggered there. Enzyme's simulate works only on React listeners, not raw DOM. See https://stackoverflow.com/questions/33638385/simulate-keydown-on-document-for-jest-unit-testing , I suppose that's your case. – Estus Flask Jul 16 '20 at 06:11
  • @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 Jul 16 '20 at 06:38
  • `it('should handle useEffect', () => { jest.spyOn(React, 'useEffect').mockImplementation(f => f()); const event = new KeyboardEvent('keydown', {'key': 'Escape'}); document.dispatchEvent(event); const component = shallow(); expect(component).toMatchSnapshot(); });` – Kumar Pranay Jul 16 '20 at 06:40
  • 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`. – Estus Flask Jul 16 '20 at 07:21

1 Answers1

3

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);
Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • 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);` – Kumar Pranay Jul 16 '20 at 08:16
  • 1
    I see. There's ongoing problem with `shallow` that requires to patch useEffect similarly to how you did, https://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 https://github.com/mikeborozdin/jest-react-hooks-shallow – Estus Flask Jul 16 '20 at 09:29