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.
Related
I am using Chakra UI and I have to mock just one hook, because I want to simulate various viewports.
My component is using the hook as following:
export const AuthenticationBase = (props: props) => {
const [isMobile] = useMediaQuery(['(max-width: 768px)']);
return (
<Box textAlign="center" fontSize={isMobile ? 'sm' : 'xl'}>
</Box>
);
}
I tried as following:
// runs before any tests start running
jest.mock("#chakra-ui/react", () => {
// --> Original module
const originalModule = jest.requireActual("#chakra-ui/react");
return {
__esModule: true,
...originalModule,
useMediaQuery: jest.fn().mockImplementation(() => [true]),
};
});
And then I execute my test:
describe('Authentication Component', () => {
it('should load with Login', () => {
const container = mount(<ChakraProvider theme={theme}><AuthenticationBase screen="login" /></ChakraProvider>);
const title = container.find('h1');
expect(title).toBeTruthy();
expect(title.text()).toBe('Login');
container.unmount();
});
}
But I get an error from JEST, somehow the hook is not mock correctly:
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
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
I'm trying to test a React component using Mocha and Enzyme that uses a dynamic import to load a module.
When I try to test the logic that relies on the dynamic import I get incorrect results. The problem is that the async functions don't finish before the test finishes so I can never get accurate results.
How can I handle this scenario?
Component
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
// styles
import styles from './PasswordStrengthIndicator.scss';
class PasswordStrengthIndicator extends React.Component {
static defaultProps = {
password: undefined,
onPasswordChange: undefined,
}
static propTypes = {
password: PropTypes.string,
onPasswordChange: PropTypes.func,
}
constructor() {
super();
this.state = {};
}
componentWillMount() {
this.handlePasswordChange();
}
componentWillReceiveProps(nextProps) {
const password = this.props.password;
const nextPassword = nextProps.password;
if (password !== nextPassword) {
this.handlePasswordChange();
}
}
render() {
const strength = this.state.strength || {};
const score = strength.score;
return (
<div className={ styles.passwordStrength }>
<div className={ classNames(styles.score, styles[`score-${score}`]) } />
<div className={ styles.separator25 } />
<div className={ styles.separator50 } />
<div className={ styles.separator75 } />
</div>
);
}
// private
async determineStrength() {
const { password } = this.props;
const zxcvbn = await import('zxcvbn');
let strength = {};
if (password) strength = zxcvbn(password);
return strength;
}
async handlePasswordChange() {
const { onPasswordChange } = this.props;
const strength = await this.determineStrength();
this.setState({ strength });
if (onPasswordChange) onPasswordChange(strength);
}
}
export default PasswordStrengthIndicator;
Test
describe('when `password` is bad', () => {
beforeEach(() => {
props.password = 'badpassword';
});
it.only('should display a score of 1', () => {
const score = indicator().find(`.${styles.score}`);
expect(score.props().className).to.include(styles.score1); // should pass
});
});
I was able to accomplish this with a -- something.
I switched the test that relies on the dynamic import to be asynchronous. I then created a function that renders the component and returns a promise that dynamically imports the module I'm trying to import in the component.
const render = () => {
indicator = shallow(
<PasswordStrengthIndicator { ...props } />,
);
return (
Promise.resolve()
.then(() => import('zxcvbn'))
);
};
I believe this is relying on the same concept as just waiting since import('zxcvbn') will take a similar enough amount of time to import in both places.
Here's my test code:
describe('when `password` is defined', () => {
describe('and password is bad', () => {
beforeEach(() => {
props.password = 'badpassword';
});
it('should display a score of 1', (done) => {
render()
.then(() => {
const score = indicator.find(`.${styles.score}`);
expect(score.props().className).to.include(styles.score1);
done();
});
});
});
});
This ended up working out because it prevented me from having to stub and I didn't have to change my component's implementation to better support stubbing. It is also less arbitrary than waiting x ms.
I'm going to leave this question open for now as there are probably better solutions that the community can provide.
Theres a couple of ways to solve this, the simple cheap and dirty way is to delay the expectation for a reasonable amount of time. This turns the test into an async test so you will need to use the done method after your assertion to tell mocha that the test is complete...
it('should display a score of 1', (done) => {
setTimeout(() => {
const score = indicator().find(`.${styles.score}`);
expect(score.props().className).to.include(styles.score1);
done() // informs Mocha that the async test should be complete, otherwise will timeout waiting
}, 1000) // mocha default timeout is 2000ms, so can increase this if necessary
});
The other more involved way would be to stub the call to import with something like Sinon and manually return a resolved promise with the dynamically loaded component.
Must admit I haven't tried stubbing a webpack method before so may be more trouble than usual. Try the simple version and see how it goes.
I faced a problem with my jest+enzyme mount() testing. I am testing a function, which switches displaying components.
Switch between components: when state.infoDisplayContent = 'mission' a missionControl component is mounted, when state.infoDisplayContent = 'profile' - other component steps in:
_modifyAgentStatus () {
const { currentAgentProfile, agentsDatabase } = this.state;
const agentToMod = currentAgentProfile;
if (agentToMod.status === 'Free') {
this.setState({
infoDisplayContent: 'mission'
});
agentToMod.status = 'Waiting';
} else if (agentToMod.status === 'Waiting') {
const locationSelect = document.getElementById('missionLocationSelect');
agentToMod.location = locationSelect[locationSelect.selectedIndex].innerText;
agentToMod.status = 'On Mission';
this.setState({
infoDisplayContent: 'profile'
});
}
}
When I trigger this function everything looks Ok, this test runs well and test successfully pass with required component:
import React from 'react';
import { mount } from 'enzyme';
import App from '../containers/App';
const result = mount(
<App />
)
test('change mission controls', () => {
expect(result.state().currentAgentProfile.status).toBe('Free');
result.find('#statusController').simulate('click');
expect(result.find('#missionControls')).toHaveLength(1);
expect(result.find('#missionLocationSelect')).toHaveLength(1);
expect(result.state().currentAgentProfile.status).toBe('Waiting');
});
But when I simulate onClick two times:
test('change mission controls', () => {
expect(result.state().currentAgentProfile.status).toBe('Free');
result.find('#statusController').simulate('click');
expect(result.find('#missionControls')).toHaveLength(1);
expect(result.find('#missionLocationSelect')).toHaveLength(1);
expect(result.state().currentAgentProfile.status).toBe('Waiting');
result.find('#statusController').simulate('click');
expect(result.state().currentAgentProfile.status).toBe('On Mission');
});
I get this assert:
TypeError: Cannot read property 'selectedIndex' of null
at App._modifyAgentStatus (development/containers/App/index.js:251:68)
at Object.invokeGuardedCallback [as invokeGuardedCallbackWithCatch] (node_modules/react-dom/lib/ReactErrorUtils.js:26:5)
at executeDispatch (node_modules/react-dom/lib/EventPluginUtils.js:83:21)
at Object.executeDispatchesInOrder (node_modules/react-dom/lib/EventPluginUtils.js:108:5)
at executeDispatchesAndRelease (node_modules/react-dom/lib/EventPluginHub.js:43:22)
at executeDispatchesAndReleaseSimulated (node_modules/react-dom/lib/EventPluginHub.js:51:10)
at forEachAccumulated (node_modules/react-dom/lib/forEachAccumulated.js:26:8)
at Object.processEventQueue (node_modules/react-dom/lib/EventPluginHub.js:255:7)
at node_modules/react-dom/lib/ReactTestUtils.js:350:22
at ReactDefaultBatchingStrategyTransaction.perform (node_modules/react-dom/lib/Transaction.js:140:20)
at Object.batchedUpdates (node_modules/react-dom/lib/ReactDefaultBatchingStrategy.js:62:26)
at Object.batchedUpdates (node_modules/react-dom/lib/ReactUpdates.js:97:27)
at node_modules/react-dom/lib/ReactTestUtils.js:348:18
at ReactWrapper.<anonymous> (node_modules/enzyme/build/ReactWrapper.js:776:11)
at ReactWrapper.single (node_modules/enzyme/build/ReactWrapper.js:1421:25)
at ReactWrapper.simulate (node_modules/enzyme/build/ReactWrapper.js:769:14)
at Object.<anonymous> (development/tests/AgentProfile.test.js:26:38)
at process._tickCallback (internal/process/next_tick.js:109:7)
It is obvious that:
document.getElementById('missionLocationSelect');
return null, but I can not get why. Element passes tests, as I mention.
expect(result.find('#missionLocationSelect')).toHaveLength(1);
But it could not be captured with document.getElementById().
Please, help me to fix this problem and run tests.
Found the solution thanks to https://stackoverflow.com/users/853560/lewis-chung and gods of Google:
Attached my component to DOM via attachTo param:
const result = mount(
<App />, { attachTo: document.body }
);
Changed buggy string in my method to string which works with element Object
agentToMod.location = locationSelect.options[locationSelect.selectedIndex].text;` :
_modifyAgentStatus () {
const { currentAgentProfile, agentsDatabase } = this.state;
const agentToMod = currentAgentProfile;
if (agentToMod.status === 'Free') {
this.setState({
infoDisplayContent: 'mission'
});
agentToMod.status = 'Waiting';
} else if (agentToMod.status === 'Waiting') {
const locationSelect = document.getElementById('missionLocationSelect');
agentToMod.location = agentToMod.location = locationSelect.options[locationSelect.selectedIndex].text;
agentToMod.status = 'On Mission';
this.setState({
infoDisplayContent: 'profile'
});
}
}
attachTo: document.body will generate a warning:
Warning: render(): Rendering components directly into document.body is discouraged, since its children are often manipulated by third-party scripts and browser extensions. This may lead to subtle reconciliation issues. Try rendering into a container element created for your app.
So just attach to a container element instead of document.body, and no need to add it to the global Window object
before(() => {
// Avoid `attachTo: document.body` Warning
const div = document.createElement('div');
div.setAttribute('id', 'container');
document.body.appendChild(div);
});
after(() => {
const div = document.getElementById('container');
if (div) {
document.body.removeChild(div);
}
});
it('should display all contents', () => {
const wrapper = mount(<YourComponent/>,{ attachTo: document.getElementById('container') });
});
Attached your component to DOM via attachTo param.
import { mount} from 'enzyme';
// Avoid Warning: render(): Rendering components directly into document.body is discouraged.
beforeAll(() => {
const div = document.createElement('div');
window.domNode = div;
document.body.appendChild(div);
})
test("Test component with mount + document query selector",()=>{
const wrapper = mount(<YourComponent/>,{ attachTo: window.domNode });
});
why we need this?
mount only render component to div element not attached it to DOM tree.
// Enzyme code of mount renderer.
createMountRenderer(options) {
assertDomAvailable('mount');
const domNode = options.attachTo || global.document.createElement('div');
let instance = null;
return {
render(el, context, callback) {
if (instance === null) {
const ReactWrapperComponent = createMountWrapper(el, options);
const wrappedEl = React.createElement(ReactWrapperComponent, {
Component: el.type,
props: el.props,
context,
});
instance = ReactDOM.render(wrappedEl, domNode);
if (typeof callback === 'function') {
callback();
}
} else {
instance.setChildProps(el.props, context, callback);
}
},
unmount() {
ReactDOM.unmountComponentAtNode(domNode);
instance = null;
},
getNode() {
return instance ? instanceToTree(instance._reactInternalInstance).rendered : null;
},
simulateEvent(node, event, mock) {
const mappedEvent = mapNativeEventNames(event);
const eventFn = TestUtils.Simulate[mappedEvent];
if (!eventFn) {
throw new TypeError(`ReactWrapper::simulate() event '${event}' does not exist`);
}
// eslint-disable-next-line react/no-find-dom-node
eventFn(ReactDOM.findDOMNode(node.instance), mock);
},
batchedUpdates(fn) {
return ReactDOM.unstable_batchedUpdates(fn);
},
};
}