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>);
};
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
The solution from slideshowp2 didn't work for me, so ended up using a different approach:
Worked around it by
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} />
}
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} />,
)
...
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)
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
Related
I want to write a test to mock a child component in a React component hierarchy using Jest. One of Jests main features is mocking functions to reduce test complexity but I cant find a straightforward way to mock child components. I can mock a parent component imported from a module OK the problem I have is mocking a child. Sorry I cant provide a codesandbox or similar as there are a few outstanding issues between csb and jest integration. Other thing to note is that I am using react testing library for rendering and assertions.
A very simple component hierarchy:
// ./features/test-samples/tree.js
export const Child = () => {
return (
<h1>Child</h1>
)
}
export const Tree = () => {
return (<>
<h1>Tree</h1>
<Child/>
</>)
}
Both of the following tests succeed.
// tree.test.js
import { render as rtlRender } from '#testing-library/react';
import { Tree } from './features/test-samples/tree';
describe.only('Test <Tree/> component', () => {
// test success
test("test entire render tree", () => {
const { getByText } = rtlRender(<Tree />);
expect(getByText(/Tree/i)).toBeInTheDocument();
expect(getByText(/Child/i)).toBeInTheDocument();
});
// test success
test("test mocked parent", () => {
jest.doMock('./features/test-samples/tree', () => {
return {
Tree: jest.fn(() => <div>Mocked Tree</div>),
}
});
const { Tree } = require('./features/test-samples/tree');
const { getByText } = rtlRender(<Tree />);
expect(getByText(/Mocked Tree/i)).toBeInTheDocument();
});
})
This doesnt work. How can I write jest test to render parent (Tree) and mock child?
import { render as rtlRender } from '#testing-library/react';
import { Tree } from './features/test-samples/tree';
...
test("test mock child", () => {
const actualModule = jest.requireActual('./features/test-samples/tree');
jest.doMock('./features/test-samples/tree', () => {
return {
...actualModule,
Child: jest.fn(() => <div>Mocked Child</div>),
}
});
const { Tree } = require('./features/test-samples/tree');
const { getByText } = rtlRender(<Tree />);
expect(getByText(/Mocked Child/i)).toBeInTheDocument();
});
error:
Test <Tree/> › test mock child
mockConstructor(...): Nothing was returned from render. This usually means a return statement is missing. Or, to render nothing, return null.
106 | });
107 | const { Tree } = require('./features/test-samples/tree');
> 108 | const { getByText } = rtlRender(<Tree />);
| ^
109 | expect(getByText(/Child/i)).toBeInTheDocument();
110 | });
111 | })
Any advice is much appreciated.
I want to isolate the test to a targeted useState.
Lets say I have 3 useStates, of which some are in my component and some are in children components in this testcase.
Currently this logs for 3 different useStates. How to target the one I want. Lets say its called setMovies.
const createMockUseState = <T extends {}>() => {
type TSetState = Dispatch<SetStateAction<T>>;
const setState: TSetState = jest.fn((prop) => {
// if setMovies ???
console.log('jest - spy mock = ', prop);
});
type TmockUseState = (prop: T) => [T, TSetState];
const mockUseState: TmockUseState = (prop) => [prop, setState];
const spyUseState = jest.spyOn(React, 'useState') as jest.SpyInstance<[T, TSetState]>;
spyUseState.mockImplementation(mockUseState);
};
interface Props {
propertyToTest: boolean
};
describe('Search Movies', () => {
describe('Onload - do first search()', () => {
beforeAll(async () => {
createMockUseState<PROPS>();
wrapper = mount(
<ProviderMovies>
<SearchMovies />
</ProviderMovies>
);
await new Promise((resolve) => setImmediate(resolve));
await act(
() =>
new Promise<void>((resolve) => {
resolve();
})
);
});
});
});
as we know react hooks depends on each initialization position. And for example if you have 3 hooks inside your component and you want to mock the 2-nd, you should mock 1 and 2 with necessary data.
Something like this
//mock for test file
jest.mock(useState); // you should mock here useState from React
//mocks for each it block
const useMockHook = jest.fn(...);
jest.spyOn(React, 'useState').mockReturnValueOnce(useMockHook);
expect(useMockHook).toHaveBeenCalled();
// after that you can check whatever you need
I have a React Scroll to Top component where we add this component Below our Router so that while moving across page we donot maintain scroll position .
I was trying to write Test case for this Component But Jest and Enzyme dosent seem to recognise this as a component when doing shallow rendering. I am using typescript and this is the component.
scrollToTop.ts
export const ScrollToTop = ({history}: IRouterResetScroll) => {
useEffect(() => {
const unListen = history.listen(() => {
window.scrollTo(0, 0);
});
return () => {
unListen();
}
}, []);
return null;
}
export default withRouter(ScrollToTop);
Here is my unit test strategy, the hardest part for your code to be tested is the history.listen(handler), so we can mock the implementation of history.listen method, we defined a queue to store the handlers. After mount the component, the mocked history will execute history.listen with a function as parameter. This function will be stored in the queue we defined before. We can get this function from the queue in unit test case and trigger it manually.
index.tsx:
import { useEffect } from 'react';
import { withRouter } from 'react-router-dom';
type IRouterResetScroll = any;
export const ScrollToTop = ({ history }: IRouterResetScroll) => {
useEffect(() => {
const unListen = history.listen(() => {
window.scrollTo(0, 0);
});
return () => {
unListen();
};
}, []);
return null;
};
export default withRouter(ScrollToTop);
index.spec.tsx:
import React from 'react';
import { ScrollToTop } from './';
import { mount } from 'enzyme';
describe('ScrollToTop', () => {
it('should scroll to top', () => {
const queue: any[] = [];
const mUnListen = jest.fn();
const mHistory = {
listen: jest.fn().mockImplementation(fn => {
queue.push(fn);
return mUnListen;
})
};
window.scrollTo = jest.fn();
const wrapper = mount(<ScrollToTop history={mHistory}></ScrollToTop>);
queue[0]();
expect(mHistory.listen).toBeCalledWith(expect.any(Function));
expect(window.scrollTo).toBeCalledWith(0, 0);
wrapper.unmount();
expect(mUnListen).toBeCalledTimes(1);
});
});
Unit test result with 100% coverage:
PASS src/stackoverflow/58786973/index.spec.tsx
ScrollToTop
✓ should scroll to top (39ms)
-----------|----------|----------|----------|----------|-------------------|
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: 1 passed, 1 total
Snapshots: 0 total
Time: 4.041s, estimated 9s
Source code: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/58786973
I have a functional component in my React code as below:
const Compo = ({funcA}) => {
useEffect(() => {
window.addEventListener('x', funcB, false);
return () => {
window.removeEventListener('x', funcB, false);
}
});
const funcB = () => {funcA()};
return (
<button
onClick={() => funcA()}
/>
);
};
Compo.propTypes = {
funcA: func.isRequired
}
export default Compo;
I need to test the above functional component to make sure the event listeners are added and removed as mentioned in the useEffect() hook.
Here is what my test file looks like -
const addEventSpy = jest.spyOn(window, 'addEventListener');
const removeEventSpy = jest.spyOn(window, 'removeEventListener');
let props = mockProps = {funcA: jest.fn()};
const wrapper = mount(<Compo {...props} />);
const callBack = wrapper.instance().funcB; <===== ERROR ON THIS LINE
expect(addEventSpy).toHaveBeenCalledWith('x', callBack, false);
wrapper.unmount();
expect(removeEventSpy).toHaveBeenCalledWith('x', callBack, false);
However, I get the below error on the line where I declare the 'callBack' constant (highlighted above in the code) :
TypeError: Cannot read property 'funcB' of null
Effectively, it renders the component ok, but wrapper.instance() is evaluating as null, which is throwing the above error.
Would anyone please know what am I missing to fix the above error?
This is my unit test strategy:
index.tsx:
import React, { useEffect } from 'react';
const Compo = ({ funcA }) => {
useEffect(() => {
window.addEventListener('x', funcB, false);
return () => {
window.removeEventListener('x', funcB, false);
};
}, []);
const funcB = () => {
funcA();
};
return <button onClick={funcB} />;
};
export default Compo;
index.spec.tsx:
import React from 'react';
import { mount } from 'enzyme';
import Compo from './';
describe('Compo', () => {
afterEach(() => {
jest.restoreAllMocks();
});
it('should call funcA', () => {
const events = {};
jest.spyOn(window, 'addEventListener').mockImplementation((event, handle, options?) => {
events[event] = handle;
});
jest.spyOn(window, 'removeEventListener').mockImplementation((event, handle, options?) => {
events[event] = undefined;
});
const mProps = { funcA: jest.fn() };
const wrapper = mount(<Compo {...mProps}></Compo>);
expect(wrapper.find('button')).toBeDefined();
events['x']();
expect(window.addEventListener).toBeCalledWith('x', expect.any(Function), false);
expect(mProps.funcA).toBeCalledTimes(1);
wrapper.unmount();
expect(window.removeEventListener).toBeCalledWith('x', expect.any(Function), false);
});
});
Unit test result with 100% coverage:
PASS src/stackoverflow/57797518/index.spec.tsx (8.125s)
Compo
✓ should call funcA (51ms)
-----------|----------|----------|----------|----------|-------------------|
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: 1 passed, 1 total
Snapshots: 0 total
Time: 9.556s
Source code: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/57797518
I hope someone can point me in the right direction to test useRef in the component below.
I have a component structured something like below. I am trying to test the functionality within the otherFunction() but I'm not sure how to mock the current property that comes off the component ref. Has anyone done something like this before?
const Component = (props) => {
const thisComponent = useRef(null);
const otherFunction = ({ current, previousSibling }) => {
if (previousSibling) return previousSibling.focus();
if (!previousSibling && current) return current.focus();
}
const handleFocus = () => {
const {current} = thisComponent;
otherFunction(current);
}
return (
<div ref={thisComponent} onFocus={handleFocus}>Stuff In here</div>
);
};
Here is my test strategy for your case. I use jest.spyOn method to spy on React.useRef hook. It will let us mock the different return value of ref object for SFC.
index.tsx:
import React, { RefObject } from 'react';
import { useRef } from 'react';
export const Component = props => {
const thisComponent: RefObject<HTMLDivElement> = useRef(null);
const otherFunction = ({ current, previousSibling }) => {
if (previousSibling) return previousSibling.focus();
if (!previousSibling && current) return current.focus();
};
const handleFocus = () => {
const { current } = thisComponent;
const previousSibling = current ? current.previousSibling : null;
otherFunction({ current, previousSibling });
};
return (
<div ref={thisComponent} onFocus={handleFocus}>
Stuff In here
</div>
);
};
index.spec.tsx:
import React from 'react';
import { Component } from './';
import { shallow } from 'enzyme';
describe('Component', () => {
const focus = jest.fn();
beforeEach(() => {
jest.restoreAllMocks();
jest.resetAllMocks();
});
test('should render correctly', () => {
const wrapper = shallow(<Component></Component>);
const div = wrapper.find('div');
expect(div.text()).toBe('Stuff In here');
});
test('should handle click event correctly when previousSibling does not exist', () => {
const useRefSpy = jest.spyOn(React, 'useRef').mockReturnValueOnce({ current: { focus } });
const wrapper = shallow(<Component></Component>);
wrapper.find('div').simulate('focus');
expect(useRefSpy).toBeCalledTimes(1);
expect(focus).toBeCalledTimes(1);
});
test('should render and handle click event correctly when previousSibling exists', () => {
const useRefSpy = jest.spyOn(React, 'useRef').mockReturnValueOnce({ current: { previousSibling: { focus } } });
const wrapper = shallow(<Component></Component>);
wrapper.find('div').simulate('focus');
expect(useRefSpy).toBeCalledTimes(1);
expect(focus).toBeCalledTimes(1);
});
test('should render and handle click event correctly when current does not exist', () => {
const useRefSpy = jest.spyOn(React, 'useRef').mockReturnValueOnce({ current: null });
const wrapper = shallow(<Component></Component>);
wrapper.find('div').simulate('focus');
expect(useRefSpy).toBeCalledTimes(1);
expect(focus).not.toBeCalled();
});
});
Unit test result with 100% coverage:
PASS src/stackoverflow/56739670/index.spec.tsx (6.528s)
Component
✓ should render correctly (10ms)
✓ should handle click event correctly when previousSibling does not exist (3ms)
✓ should render and handle click event correctly when previousSibling exists (1ms)
✓ should render and handle click event correctly when current does not exist (2ms)
-----------|----------|----------|----------|----------|-------------------|
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: 4 passed, 4 total
Snapshots: 0 total
Time: 7.689s
Source code: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/56739670