4

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?

9
  • 1
    Hooks weren't designed to test the implementation. Test the behaviour. Commented Jul 12, 2020 at 5:40
  • Possible duplicate of stackoverflow.com/questions/38960832/… Commented Jul 12, 2020 at 5:41
  • @EstusFlask, I was able to get this test case covered by mocking useEffect and useCallback. jest.spyOn(React, 'useEffect').mockImplementation(f => f()); Commented Jul 15, 2020 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. Commented Jul 15, 2020 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? Commented Jul 16, 2020 at 3:59

1 Answer 1

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);
Sign up to request clarification or add additional context in comments.

2 Comments

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);
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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.