I am using jest and enzyme to test my react component which relies on antd - a 3rd react UI library.
For illustrative purpose, I produce the minimal code that is fair enough to show my question.
See my test component as follow:
import React from 'react';
import { Button } from 'antd';
function Test({onSubmit}) {
return (
<div>
<Button htmlType="submit" type="primary" />
</div>
);
}
export default Test;
Q1: I mock up the Button as below since unit test requires us to isolate the target and mock up any other dependencies.
Is that correct?
__mocks__
antd.js
antd.js
import mockComponent from '../mockComponent';
const list = [
'Form',
'Input',
'Button',
'Spin',
'Icon'
];
const mockups = list.reduce((prev, next) => {
prev[next] = mockComponent(next);
return prev;
}, {});
mockups.Form.Item = mockComponent('Form.Item');
export const Form = mockups.Form;
export const Input = mockups.Input;
export const Button = mockups.Button;
export const Spin = mockups.Spin;
export const Icon = mockups.Icon;
mockComponent.js
import React from 'react';
export default function mockComponent (name) {
return props => React.createElement(name, props, props.children);
}
Q2. I have got the following warning even if the test passes. How can I resolve that?
test.spec.js
import React from 'react';
import { shallow, mount } from 'enzyme';
import renderer from 'react-test-renderer';
import Test from '../components/question';
describe('mount test', () => {
let wrapper;
let props;
let mountedLogin;
const test = () => {
if (!mountedLogin) {
mountedLogin = mount(<Test {...props} />);
}
return mountedLogin;
};
beforeEach(() => {
mountedLogin = null;
});
it('always render test as the root', () => {
const divs = test().find('div');
expect(divs.length).toBeGreaterThan(0);
});
});
warning
console.error node_modules/fbjs/lib/warning.js:36
Warning: Unknown prop `htmlType` on <Button> tag. Remove this prop from the
element. For details, see
in Button (created by Unknown)
in Unknown (created by Test)
in div (created by Test)
in Test (created by WrapperComponent)
in WrapperComponent
A side note, I haven't got any jest configs in my package.json.
Can anybody help me out with these.
Many thanks
Q1: I mock up the Button as below since unit test requires us to
isolate the target and mock up any other dependencies. Is that
correct?
Currently, the recommended approach for React unit testing is shallow rendering. It basically renders the given component one level deep. If we shallow render your Test component, it will call render method of Test component, but not the render method of Button component. Even though Button is 3rd party component and dependency, we don't need to mock it. Because we don't render it. But still, we will be able to see whether it's in the component tree and it has got the correct props. This is what essentially we will assert with the mocking approach also. However, shallow rendering has few limitations also, but usually, it works very well for most of the scenarios.
But you can obviously mock children and render the whole component also. But it's time-consuming and problematic, at least with my experience.
Q2: I have got the following warning even if the test passes. How can
I resolve that?
Since you pass a string as name for React.createElement, React think you want to create a normal HTML element, and not a React component. But since there is a no HTML element call Button (case sensitive) and it doesn't know prop called htmlType, you get this unknown prop type warning. To prevent this warning, either you can stop passing props to React.createElement or pass a mock component to React.createElement instead of the name string.
import React from 'react';
function Mock(){
retun (<div/>);
}
export default function mockComponent (name) {
return props => {
return React.createElement(Mock, {...props, name}, props.children);
}
}
If you need to read more about react unit testing, I suggest you to go through this thread from react discuss forum.
Hope this helps!
Related
I am having a lot of trouble trying to implement tests for a component using the useSelector hook from react redux. I've seen some questions already about this subject but I didn't manage to fix my problem using the suggested solutions to those questions.
My component is pretty big so I won't post it all but the part giving me trouble looks like this :
Total.tsx
import React from 'react';
import clsx from 'clsx';
import i18next from 'i18next';
import { useSelector } from 'react-redux';
import { Trans } from 'react-i18next';
import Box from '#material-ui/core/Box';
import CustomTooltip from '../CustomTooltip/CustomTooltip';
import SkeletonTotal from 'components/Skeletons/Total';
import { ApplicationHelper } from 'helpers';
import './Total.scss';
//Some interfaces here for types since this is in TypeScript
function Total(props: TotalProps) {
const { currency } = useSelector(
(state: { currencyReducer: any }) => state.currencyReducer
);
...
}
I first tried to test it like another component that doesn't use redux like so :
Total.test.js (first attempt)
import React from 'react';
import Total from './Total';
import { render } from '#testing-library/react';
test('test', () => {
const { container } = render(
<Total priceLoading={false} bookingPrice={bookingPrice} values={myFormValues} />
);
});
But I was getting an error saying I need a react-redux context value and to wrap my component in a Provider which led me to try this :
Total.test.js (attempt 2)
import React from 'react';
import { Provider } from 'react-redux'
import Total from './Total';
import { render } from '#testing-library/react';
test('test', () => {
const { container } = render(
<Provider>
<Total priceLoading={false} bookingPrice={bookingPrice} values={myFormValues} />
</Provider>
);
});
I am now getting a "Cannot read property 'getState' of undefined" error for the Provider component. I did try to mock a store to pass to my Provider as well as using jest to mock a return value like so
const spy = jest.spyOn(redux, 'useSelector')
spy.mockReturnValue({currency: 'cad'})
Unfortunately I was unsuccessful to make this work and could not find a working solution in the other questions that might relate to this. Any ideas how I could make this work? Thanks
The useSelector hook relies on the redux Context in order to access the state, so it must be inside of a Provider component in order to work. Your second attempt is on the right track, but you haven't set the store prop on the Provider, so the store is undefined and you get error "Cannot read property 'getState' of undefined".
Since you'll likely have many components that you'll want to test with redux context, the redux docs suggest creating your own version of the react testing library's render function which wraps the element in a provider before rendering it. This new render function adds two new optional options to the standard RTL options: initialState and store.
You can basically copy and paste that entire test-utils.js example from the docs, but I modified the return to include the created store so that we can dispatch to it directly (rather than just interacting with the component in ways that will dispatch an action).
return {
...rtlRender(ui, { wrapper: Wrapper, ...renderOptions }),
store
};
With typescript annotations.
Inside your component test file, you will use your test-utils to render the Total component. It's fine to return the container element but you don't actually need to because you can query matching elements on the global RTL screen object or on the bound queries for your base element. We are basically looking to see that the outputted HTML code matches the expectations. You could test the selector itself in isolation, but it seems like you are trying to test the component.
Your test might look something like this:
import React from "react";
import Total from "./Total";
import { render, screen } from "./test-utils";
// if you want events: import userEvent from "#testing-library/user-event";
test( 'gets currency from redux', () => {
// render with an initial currency
const { store, container, getByLabelText } = render(
// not sure where these props come from, presumable constants in the file
<Total priceLoading={false} bookingPrice={bookingPrice} values={myFormValues} />,
{ initialState: { currency: USD } }
);
// some sort of RTL matcher or document.querySelector
const currencyElement = getByLabelText(/currency/i); // uses regex for case-insensitivity
// some sort of check for value
expect(currencyElement?.innerText).toBe("USD");
// dispatch an action to change the currency
// might want to wrap in `act`?
store.dispatch(setCurrency("EUR"));
// check that the value changed
expect(currencyElement?.innerText).toBe("EUR");
});
Working example that I created based on a basic counter component.
Trying to test the Material-UI Slider with Reat-Test-Renderer gets an error: Uncaught [TypeError: Cannot read property 'addEventListener' of null]
Codesandbox Link
import React from "react";
import { Slider } from "#material-ui/core";
import renderer from "react-test-renderer";
it("should render", () => {
renderer.create(<Slider />);
});
This is not the case with any other Material UI components that I know of.
It seems to be related to forwardRef as described here, but I could not figure out a way to get it to work.
EDIT Unfortunately switching over to #testing-library/react is not an option this project I'm working on.
EDIT 2 The reason I am doing this is because I am trying to render and test a more complex component of my own which contains the Slider. It took me a while to figure out that this is what's causing the issue, and the code above is the minimal amount of code to replicate the issue.
EDIT 3 Error message screenshot
React Test Renderer only renders the components as a JS object rather than to a browser environment. Hence, anything that uses dom based refs will have this issue. Related jest issue: https://github.com/facebook/jest/issues/5462.
There is a section in the React Docs for a way to workaround it: https://reactjs.org/docs/test-renderer.html#ideas
EDIT:
Here's a working test. It likely makes a lot of the functionality of the slider not work, since we are setting the internal refs to null. But it does run. The "Next" branch on MUI doesn't have the findDOMNode function there, so that might be easier to work with.
I wasn't able to get these to work on CodeSandbox because jest was undefined, and I couldn't find how to fix that.
import React from 'react';
import { Slider } from '#material-ui/core';
import renderer from 'react-test-renderer';
jest.mock('react-dom', () => ({
// the useIsFocusVisible function in Slider.js (MUI component) uses findDOMNOde
// Luckily it checks if there's nulls, so we can return a null
// https://github.com/mui-org/material-ui/blob/master/packages/material-ui/src/utils/useIsFocusVisible.js#L136
findDOMNode: () => null,
}));
it('should render', () => {
renderer.create(<Slider />);
});
If you want to try and mock the full functionality (sort of), you can use this implementation of the findDOMNode mock, which will return the values that are needed for the useIsFocusVisible to run successfully including adding the event listener:
import React from 'react';
import { Slider } from '#material-ui/core';
import renderer from 'react-test-renderer';
jest.mock('react-dom', () => ({
// the useIsFocusVisible function in Slider.js (MUI component) uses findDOMNOde
// https://github.com/mui-org/material-ui/blob/master/packages/material-ui/src/utils/useIsFocusVisible.js#L136
findDOMNode: (instance) => {
return { ownerDocument: instance };
},
}));
it('should render', () => {
let eventListenerFn = jest.fn();
renderer.create(<Slider />, {
createNodeMock: (element) => {
if (element.type === 'span') {
return {
addEventListener: eventListenerFn,
};
}
},
});
});
In order to figure out how to get these tests to succesfully run, I had to step through the error stacks/MUI source code to find out what's going wrong and what values were needed to be mocked in order to not throw errors.
Original, showing how much better #testing-library is for this:
However, may I suggest using #testing-library? It's nice to work with and uses jsdom to render the components so it'll work with this component.
I went ahead and made a couple of random tests for the Slider using the testing library to show that it works with Slider and throws no errors:
import React from "react";
import { Slider } from "#material-ui/core";
import { render } from "#testing-library/react";
it("should render", () => {
render(<Slider />);
});
it("should render an input", () => {
const component = render(<Slider />);
const input = component.baseElement.querySelector("input");
expect(input).toBeDefined();
});
it("Should have the correct value", () => {
const component = render(<Slider value={50} />);
const input = component.baseElement.querySelector("input");
expect(input.value).toBe("50");
});
CodeSandbox
I am writing a test for a component that is wrapped in a withStyles() from Material UI using Jest. I need to test the children elements, but my wrapper is undefined.
I've seen another post similar to writing tests with withStyles(), but the undefined error still persists.
Test File:
import { shallow } from 'enzyme';
import { TempComponent } from '../../../../src/components/helpers/temp';
describe('temp', () => {
let wrapper;
const renderComponent = () => shallow(<TempComponent />);
beforeEach(() => {
wrapper = renderComponent();
});
it('render correctly', () => {
expect(wrapper.type()).toEqual('div');
});
});
Component:
import React from 'react';
import { withStyles } from '#material-ui/core/styles';
const TempComponent = () => <button>Click Me!</button>;
export default withStyles({})(TempComponent);
I get this error for my test:
Invariant Violation: ReactShallowRenderer render(): Shallow rendering works only with custom components, but the provided element type was undefined.
I am wanting the wrapper component to behave the same way as a wrapper without the withStyles() component. Any help would be appreciated!
Note: I am not doing snapchat testing with jest
This appears to be the issue and solution:
https://github.com/mui-org/material-ui/issues/9266#issuecomment-349447137
The issue isn't about the children elements but with the intermediary
element, it's creating. The shallow() API of enzyme only render one
level depth. Any higher-order component is going to obfuscate the
children. You have different workarounds. I would encourage you to
refer to the enzyme documentation. Still, here they are:
use mount()
use .dive()
use our createShallow() public API
use our internal until() helper
I am using enzyme. I need to test a component that has react router Link as a child. I need the following
mount the component using mount() of enzyme since I need to test the whole component tree
test behaviours of component when it properties change.
I cannot wrap my component with StaticRouter or MemoryRouter since enzyme only allows setProps() at root level.
My current solution is to stub the Link render method with sinon. Here is a short example.
import {Link} from 'react-router-dom';
import sinon from 'sinon';
// ....
// ....
describe('test',() => {
before(() => {
sinon.stub(Link.prototype, 'render').callsFake(function reactRouterLinkRender() {
const {innerRef, replace, to } = this.props;
const _props = {href: to, ref: innerRef, replace, onClick: this.handleClick};
return (<a {..._props}>this.props.children</a>);
});
});
});
Is there a better way to avoid the error "Invariant Violation: You should not use Link outside a Router"?
Thanks
Not sure if this helps anyone but for my case I managed to get around by shallow rendering the top level component and then using dive() to get to the parts I required.
loginPage = shallow(<LoginPage />);
....
loginPage.find(LoginForm).dive().find({name:"user"}).simulate("change", userEventMock);
I have a React component with a modal dialog (built using reactstrap, but others have reported similar problems with react-bootstrap and other types of modal components). Enzyme cannot find any of the components inside the modal, even though they render fine in the actual app. Minimal example:
import React from 'react'
import { Modal } from 'reactstrap'
export default class MyModal extends React.Component {
render() {
return (
<div className="outside"> Some elements outside of the dialog </div>
<Modal isOpen={this.props.modalOpen}>
<div className="inside"> Content of dialog </div>
</Modal>
);
}
}
I would like to test the contents (in this case using jest) like this
import React from 'react'
import MyModal from './MyModal'
import { mount } from 'enzyme'
it('renders correctly', () => {
const wrapper = mount( <MyModal modalOpen/> );
expect(wrapper).toMatchSnapshot();
// Passes
expect(wrapper.find('.outside')).toHaveLength(1);
// Fails, 0 length
expect(wrapper.find('.inside')).toHaveLength(1);
});
The test finds the contents outside of the Modal correctly, but does not find anything inside. Looking at the snapshot shows that, indeed, nothing inside the <Modal> is rendered. However it does work if I replace mount with shallow. The problem with that is I need mount to test lifecycle methods like componentDidMount.
Why doesn't mount render the contents of the modal? I thought the whole point was that it rendered the entire tree of child elements.
Edit: This is no longer a problem in React 16 + Enzyme 3, because React 16 supports portal components.
In React 15 and before, the problem is that a modal dialog is (in most implementations) a portal component. This means it creates DOM elements that are attached directly to the document root, rather than being children of the parent React component.
The find method of the ReactWrapper created by mount looks through the DOM starting with the element created by the top level component, so it can't find the contents of the modal. But Enzyme's shallow doesn't attach to a DOM, and instead builds its own component tree which contains the modal contents.
To test a portal component, you first need to find the DOM elements that have been attached to the document body. Then you can create a new ReactWrapper around them so that all the usual Enzyme functions work:
import React from 'react'
import MyModal from './MyModal'
import { mount, ReactWrapper } from 'enzyme'
it('renders correctly', () => {
const wrapper = mount( <MyModal modalOpen/> );
expect(wrapper).toMatchSnapshot();
// Passes
expect(wrapper.find('.outside')).toHaveLength(1);
// Construct new wrapper rooted at modal content
inside_els = document.getElementsByClassName("inside")[0]
inside_wrapper = new ReactWrapper(inside_els, true)
// Passes
expect(inside_wrapper.find('.inside')).toHaveLength(1);
});
Currently, this is an open bug in Enzyme.
Update: It seems that Enzyme also leaves the modal attached to the DOM after the test finishes, so you may end up with multiple dialogs open in a later test. If this is a problem, you can clear the DOM after each test like this:
afterEach(() => {
var node = global.document.body;
while (node.firstChild) {
node.removeChild(node.firstChild);
}
});
try mocking the createPortal responsible for showing modals ReactDOM.createPortal = jest.fn(modal => modal);
In case you are using an older version of Enzyme, you can pass the container element to mount where you want your Modal to be rendered, like this:
import React from 'react'
import MyModal from './MyModal'
import { mount } from 'enzyme'
describe(() => {
let wrapper;
beforeEach(() => {
const container = document.createElement("div");
document.body.appendChild(container);
wrapper = mount( <MyModal modalOpen/> , {attachTo: container});
});
it('renders correctly', () => {
expect(wrapper).toMatchSnapshot();
// Passes
expect(wrapper.find('.outside')).toHaveLength(1);
// Passes now
expect(wrapper.find('.inside')).toHaveLength(1);
});
})