Reputation: 621
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>);
};
Upvotes: 16
Views: 74593
Reputation: 31
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();
});
Upvotes: 2
Reputation: 69
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)
Upvotes: 6
Reputation: 470
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
Upvotes: 0
Reputation: 7428
The solution from slideshowp2 didn't work for me, so ended up using a different approach:
Worked around it by
import React, { useRef as defaultUseRef } from 'react'
const component = ({ useRef = defaultUseRef }) => {
const ref = useRef(null)
return <RefComponent ref={ref} />
}
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} />,
)
...
Upvotes: 8
Reputation: 102672
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
Upvotes: 13