16

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>);
};
8
  • 1
    Submits the code structure and test you tried to create for ease. Commented Sep 5, 2019 at 12:55
  • 1
    There's this issue which seems to say that you can't do it with shallow rendering Commented Sep 5, 2019 at 12:58
  • @JhonMike I have added a small example Commented Sep 5, 2019 at 13:10
  • @Shadowlauch use mount instead as shallow dont support refs Commented Sep 5, 2019 at 15:42
  • avowed useRef is not replacement for React.createRef Commented Sep 5, 2019 at 18:22

5 Answers 5

13

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

Sign up to request clarification or add additional context in comments.

7 Comments

The issue I have with this solution, it forces me to use React.useRef. But I guess this is the only way with shallow.
@Shadowlauch your comment is the key
What if the component has multiple useRefs?
@Shadowlauch Check this answer: stackoverflow.com/a/61789113/6463558
it does not seem to actually mock the ref to return a {current: null} (at least not when using mount).
|
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} />,
    )
...

1 Comment

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)

Comments

2

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

Comments

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

Comments

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.