Mocking refs in React function component
Asked Answered
M

5

15

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>);
};
Melchior answered 5/9, 2019 at 12:51 Comment(8)
Submits the code structure and test you tried to create for ease.Putput
There's this issue which seems to say that you can't do it with shallow renderingCochrane
@JhonMike I have added a small exampleMelchior
@Melchior use mount instead as shallow dont support refsIndustrials
avowed useRef is not replacement for React.createRefKaka
@Kaka what are you trying to say? In a function component I have to use the hook, otherwise a new ref gets created every renderMelchior
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 useRefKaka
for users visiting this in 2023. please check useImperativeHandle to mock useRef in your tests.Evasive
H
12

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

Halla answered 10/11, 2019 at 15:49 Comment(8)
The issue I have with this solution, it forces me to use React.useRef. But I guess this is the only way with shallow.Melchior
@Melchior your comment is the keyMagdaleno
What if the component has multiple useRefs?Haydon
@Melchior Check this answer: https://mcmap.net/q/746337/-mock-react-useref-or-a-function-inside-a-functional-component-with-enzyme-and-jestHalla
it does not seem to actually mock the ref to return a {current: null} (at least not when using mount).Barbuto
This didn't work for me , this did https://mcmap.net/q/746337/-mock-react-useref-or-a-function-inside-a-functional-component-with-enzyme-and-jestCristal
it's working well, thanksDafna
Invalid hook call. Hooks can only be called inside of the body of a function componentRebeckarebeka
E
8

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} />,
    )
...
Elisaelisabet answered 24/3, 2020 at 16:16 Comment(1)
K
6

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)
Kipper answered 1/11, 2021 at 12:3 Comment(0)
G
0

I wasn't able to get some of the answers to work so I ended up moving my useRef into its own function and then mocking that function:

// imports the refCaller from this file which then be more easily mocked
import { refCaller as importedRefCaller } from "./current-file";

// Is exported so it can then be imported within the same file
/**
* Used to more easily mock ref
* @returns ref current
*/
export const refCaller = (ref) => {
    return ref.current;
};

const Comp = () => {
    const ref = useRef(null);

    const functionThatUsesRef= () => {
        if (importedRefCaller(ref).thing==="Whatever") {
            doThing();
        };
    }

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

And then for the test a simple:

const currentFile= require("path-to/current-file");

it("Should trigger do the thing", () => {
    let refMock = jest.spyOn(fileExplorer, "refCaller");
    refMock.mockImplementation((ref) => {
        return { thing: "Whatever" };
    });

Then anything after this will act with the mocked function.

For more on mocking a function I found: https://pawelgrzybek.com/mocking-functions-and-modules-with-jest/ and Jest mock inner function helpful

Goniometer answered 15/10, 2021 at 10:12 Comment(0)
G
0

You can use renderHook and pass it as a reference to the component. Here is an example.

    it('should show and hide using imperative handle methods', async () => {
    const ref = renderHook(() => useRef<OverlayDropdownRef>(null)).result.current;

    renderWithContext(
        <OverlayDropdown
            {...overlayDropdownMock}
            ref={ref}
            testID={overlayDropdownId}
        />
    );

    // Show the modal
    ref?.current?.show();
    expect(screen.getByTestId(modalId)).toBeOnTheScreen();

    // Hide the modal
    ref?.current?.hide();
    expect(screen.queryByTestId(modalId)).not.toBeOnTheScreen();
});
Gilgilba answered 5/8 at 2:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.