Mocking refs in React function component

24,997

Solution 1

Here is my unit test strategy, use jest.spyOn method spy on the useRef hook.

index.tsx:

import React from 'react';

export const Comp = ({ onHandle }: any) => {
  const ref = React.useRef(null);

  const handleClick = () => {
    if (!ref.current) return;

    onHandle();
  };

  return (
    <button ref={ref} onClick={handleClick}>
      test
    </button>
  );
};

index.spec.tsx:

import React from 'react';
import { shallow } from 'enzyme';
import { Comp } from './';

describe('Comp', () => {
  afterEach(() => {
    jest.restoreAllMocks();
  });
  it('should do nothing if ref does not exist', () => {
    const useRefSpy = jest.spyOn(React, 'useRef').mockReturnValueOnce({ current: null });
    const component = shallow(<Comp></Comp>);
    component.find('button').simulate('click');
    expect(useRefSpy).toBeCalledWith(null);
  });

  it('should handle click', () => {
    const useRefSpy = jest.spyOn(React, 'useRef').mockReturnValueOnce({ current: document.createElement('button') });
    const mock = jest.fn();
    const component = shallow(<Comp onHandle={mock}></Comp>);
    component.find('button').simulate('click');
    expect(useRefSpy).toBeCalledWith(null);
    expect(mock).toBeCalledTimes(1);
  });
});

Unit test result with 100% coverage:

 PASS  src/stackoverflow/57805917/index.spec.tsx
  Comp
    ✓ should do nothing if ref does not exist (16ms)
    ✓ should handle click (3ms)

-----------|----------|----------|----------|----------|-------------------|
File       |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
-----------|----------|----------|----------|----------|-------------------|
All files  |      100 |      100 |      100 |      100 |                   |
 index.tsx |      100 |      100 |      100 |      100 |                   |
-----------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        4.787s, estimated 11s

Source code: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/57805917

Solution 2

The solution from slideshowp2 didn't work for me, so ended up using a different approach:

Worked around it by

  1. Introduce a useRef optional prop and by default use react's one
import React, { useRef as defaultUseRef } from 'react'
const component = ({ useRef = defaultUseRef }) => {
  const ref = useRef(null)
  return <RefComponent ref={ref} />
}
  1. in test mock useRef
const mockUseRef = (obj: any) => () => Object.defineProperty({}, 'current', {
  get: () => obj,
  set: () => {}
})

// in your test
...
    const useRef = mockUseRef({ refFunction: jest.fn() })
    render(
      <ScanBarcodeView onScan={handleScan} useRef={useRef} />,
    )
...

Solution 3

If you use ref in nested hooks of a component and you always need a certain current value, not just to the first renderer. You can use the following option in tests:

const reference = { current: null };
Object.defineProperty(reference, "current", {
    get: jest.fn(() => null),
    set: jest.fn(() => null),
});
const useReferenceSpy = jest.spyOn(React, "useRef").mockReturnValue(reference);

and don't forget to write useRef in the component like below

const ref = React.useRef(null)
Share:
24,997
Shadowlauch
Author by

Shadowlauch

Updated on November 02, 2021

Comments

  • Shadowlauch
    Shadowlauch over 2 years

    I have React function component that has a ref on one of its children. The ref is created via useRef.

    I want to test the component with the shallow renderer. I have to somehow mock the ref to test the rest of the functionality.

    I can't seem to find any way to get to this ref and mock it. Things I have tried

    • Accessing it via the childs property. React does not like that, since ref is not really a props

    • Mocking useRef. I tried multiple ways and could only get it to work with a spy when my implementation used React.useRef

    I can't see any other way to get to the ref to mock it. Do I have to use mount in this case?

    I can't post the real scenario, but I have constructed a small example

    it('should test', () => {
        const mock = jest.fn();
        const component = shallow(<Comp onHandle={mock}/>);
    
    
        // @ts-ignore
        component.find('button').invoke('onClick')();
    
        expect(mock).toHaveBeenCalled();
    });
    
    const Comp = ({onHandle}: any) => {
        const ref = useRef(null);
    
        const handleClick = () => {
            if (!ref.current) return;
    
            onHandle();
        };
    
        return (<button ref={ref} onClick={handleClick}>test</button>);
    };
    
    • Jhon Mike
      Jhon Mike almost 5 years
      Submits the code structure and test you tried to create for ease.
    • apokryfos
      apokryfos almost 5 years
      There's this issue which seems to say that you can't do it with shallow rendering
    • Shadowlauch
      Shadowlauch almost 5 years
      @JhonMike I have added a small example
    • Rikin
      Rikin almost 5 years
      @Shadowlauch use mount instead as shallow dont support refs
    • skyboyer
      skyboyer almost 5 years
      avowed useRef is not replacement for React.createRef
    • Shadowlauch
      Shadowlauch almost 5 years
      @skyboyer what are you trying to say? In a function component I have to use the hook, otherwise a new ref gets created every render
    • skyboyer
      skyboyer almost 5 years
      how do you want to use ref? if using in component's logic or event handler? or do you want to pass it outside? anyway you need to initialize ref with React.createRef regardless if it's creating on each render or saving between renders by using useRef
  • Shadowlauch
    Shadowlauch over 4 years
    The issue I have with this solution, it forces me to use React.useRef. But I guess this is the only way with shallow.
  • Julian Torregrosa
    Julian Torregrosa over 4 years
    @Shadowlauch your comment is the key
  • Drazen Bjelovuk
    Drazen Bjelovuk over 4 years
    What if the component has multiple useRefs?
  • slideshowp2
    slideshowp2 about 4 years
    @Shadowlauch Check this answer: stackoverflow.com/a/61789113/6463558
  • slideshowp2
    slideshowp2 about 4 years
  • Zargold
    Zargold over 3 years
    it does not seem to actually mock the ref to return a {current: null} (at least not when using mount).
  • Tom
    Tom about 3 years
    This didn't work for me , this did stackoverflow.com/a/61789113/168012
  • nima
    nima about 2 years
    it's working well, thanks