14

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>);
};
Lin Du
  • 88,126
  • 95
  • 281
  • 483
Shadowlauch
  • 581
  • 1
  • 5
  • 13
  • 1
    Submits the code structure and test you tried to create for ease. – Jhon Mike Sep 05 '19 at 12:55
  • 1
    There's [this issue](https://github.com/airbnb/enzyme/issues/316) which seems to say that you can't do it with shallow rendering – apokryfos Sep 05 '19 at 12:58
  • @JhonMike I have added a small example – Shadowlauch Sep 05 '19 at 13:10
  • @Shadowlauch use `mount` instead as `shallow` dont support refs – Rikin Sep 05 '19 at 15:42
  • avowed `useRef` is not replacement for `React.createRef` – skyboyer Sep 05 '19 at 18:22
  • @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 – Shadowlauch Sep 06 '19 at 05:16
  • 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` – skyboyer Sep 06 '19 at 05:40
  • for users visiting this in 2023. please check `useImperativeHandle` to mock useRef in your tests. – STEEL Jan 03 '23 at 08:23

4 Answers4

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

Lin Du
  • 88,126
  • 95
  • 281
  • 483
7

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} />,
    )
...
almeynman
  • 7,088
  • 3
  • 23
  • 37
5

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)
Ignat Romanov
  • 59
  • 1
  • 2
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

Oliver Brace
  • 393
  • 1
  • 4
  • 21