October 2017: the article was updated to React 16 and Enzyme 3.
Some people say that testing React components is useless and in many cases it is, but there are a few cases when I think it’s useful:
I’ve tried many tools and finally have found a combination that I like enough to suggest to other developers:
For the most of my tests I use shallow rendering with Jest snapshots.
Snapshot testing in Jest
Shallow rendering renders only component itself without its children. So if you change something in a child component it won’t change shallow output of your component. Or a bug, introduced to a child component, won’t break your component’s test. It also doesn’t require DOM.
For example this component:
const ButtonWithIcon = ({icon, children}) => (<button><Icon icon={icon} />{children}</button>);
Will be rendered by React like this:
<button><i class="icon icon_coffee"></i>Hello Jest!</button>
But like this with shallow rendering:
<button><Icon icon="coffee" />Hello Jest!</button>
Note that the Icon component was not rendered.
Jest snapshots are like those old text UIs with windows and buttons made of text characters: it’s a rendered output of your component stored in a text file.
You tell Jest that you want to be sure that output of this component should never change accidentally and Jest saves it to a file that looks like this:
exports[`test should render a label 1`] = `<labelclassName="isBlock">Hello Jest!</label>`;
exports[`test should render a small label 1`] = `<labelclassName="isBlock isSmall">Hello Jest!</label>`;
Every time you change your markup Jest will show you a diff and ask you to update a snapshot if the change was intended.
Jest stores snapshots besides your tests in files like __snapshots__/Label.spec.js.snap and you need to commit them with your code.
First install all the dependencies including peer dependencies:
npm install --save-dev jest react-test-renderer enzyme enzyme-adapter-react-16 enzyme-to-json
You’ll also need babel-jest for Babel and ts-jest for TypeScript.
Update your package.json:
"scripts": {"test": "jest","test:watch": "jest --watch","test:coverage": "jest --coverage"},"jest": {"setupFiles": ["./test/jestsetup.js"],"snapshotSerializers": ["enzyme-to-json/serializer"]}
snapshotSerializers allows you to pass Enzyme wrappers directly to Jest’s snapshot matcher, without converting them manually by calling enzyme-to-json’s toJson function.
Create a test/jestsetup.js file to customize Jest environment (see setupFiles above):
import Enzyme, { shallow, render, mount } from 'enzyme';import Adapter from 'enzyme-adapter-react-16';
// React 16 Enzyme adapterEnzyme.configure({ adapter: new Adapter() });
// Make Enzyme functions available in all test files without importingglobal.shallow = shallow;global.render = render;global.mount = mount;
For CSS Modules also add to jest section in your package.json:
"jest": {"moduleNameMapper": {"^.+\\.(css|scss)$": "identity-obj-proxy"}}
And run:
npm install --save-dev identity-obj-proxy
Note that identity-obj-proxy requires node — harmony-proxies flag for Node 4 and 5.
That’s enough for most non-interactive components:
test('render a label', () => {const wrapper = shallow(<Label>Hello Jest!</Label>);expect(wrapper).toMatchSnapshot();});
test('render a small label', () => {const wrapper = shallow(<Label small>Hello Jest!</Label>);expect(wrapper).toMatchSnapshot();});
test('render a grayish label', () => {const wrapper = shallow(<Label light>Hello Jest!</Label>);expect(wrapper).toMatchSnapshot();});
Sometimes you want to be more explicit and see real values in tests. In that case use Enzyme API with regular Jest assertions:
test('render a document title', () => {const wrapper = shallow(<DocumentTitle title="Events" />);expect(wrapper.prop('title')).toEqual('Events');});
test('render a document title and a parent title', () => {const wrapper = shallow(<DocumentTitle title="Events" parent="Event Radar" />);expect(wrapper.prop('title')).toEqual('Events — Event Radar');});
In some cases you just can’t use snapshots. For example if you have random IDs or something like that:
test('render a popover with a random ID', () => {const wrapper = shallow(<Popover>Hello Jest!</Popover>);expect(wrapper.prop('id')).toMatch(/Popover\d+/);});
You can simulate an event like click or change and then compare component to a snapshot:
test('render Markdown in preview mode', () => {const wrapper = shallow(<MarkdownEditor value="*Hello* Jest!" />);
expect(wrapper).toMatchSnapshot();
wrapper.find('\[name="toggle-preview"\]').simulate('click');
expect(wrapper).toMatchSnapshot();
});
Sometimes you want to interact with an element in a child component to test effect in your component. For that you need a proper DOM rendering with Enzyme’s mount method:
test('open a code editor', () => {const wrapper = mount(<Playground code={code} />);
expect(wrapper.find('.ReactCodeMirror')).toHaveLength(0);
wrapper.find('button').simulate('click');
expect(wrapper.find('.ReactCodeMirror')).toHaveLength(1);
});
Similar to events testing but instead of testing component’s rendered output with a snapshot use Jest’s mock function to test an event handler itself:
test('pass a selected value to the onChange handler', () => {const value = '2';const onChange = jest.fn();const wrapper = shallow(<Select items={ITEMS} onChange={onChange} />);
expect(wrapper).toMatchSnapshot();
wrapper.find('select').simulate('change', {
target: { value },
});
expect(onChange).toBeCalledWith(value);
});
Jest snapshots work with JSON so you can test any function that returns JSON the same way you test your components:
test('accept custom properties', () => {const wrapper = shallow(<LayoutflexBasis={0}flexGrow={1}flexShrink={1}flexWrap="wrap"justifyContent="flex-end"alignContent="center"alignItems="center"/>);expect(wrapper.prop('style')).toMatchSnapshot();});
Debugging shallow renderer output
Use Enzyme’s debug method to print shallow renderer’s output:
const wrapper = shallow(/*~*/);console.log(wrapper.debug());
Failing tests with enabled coverage
When your tests fail with — coverage flag with diff like this:
-<Button+<Component
Try to replace arrow function component with regular function:
- export default const Button = ({ children }) => {+ export default function Button({ children }) {
requestAnimationFrame error
You may see an error like this when you run your tests:
console.error node_modules/fbjs/lib/warning.js:42 Warning: React depends on requestAnimationFrame. Make sure that you load a polyfill in older browsers. http://fb.me/react-polyfills
React 16 depends on [requestAnimationFrame](https://reactjs.org/docs/javascript-environment-requirements.html)
, so you need to add a polyfill to your tests:
// test/jestsetup.jsimport 'raf/polyfill';
Thanks to Chris Pojer, Max Stoiber and Anna Gerus for proofreading and comments.
P. S. Check out my open source project: React Styleguidist, a component style guide generator with hot reloaded dev server.
Subscribe to my newsletter: https://tinyletter.com/sapegin
Hacker Noon is how hackers start their afternoons. We’re a part of the @AMIfamily. We are now accepting submissions and happy to discuss advertising &sponsorship opportunities.
To learn more, read our about page, like/message us on Facebook, or simply, tweet/DM @HackerNoon.
If you enjoyed this story, we recommend reading our latest tech stories and trending tech stories. Until next time, don’t take the realities of the world for granted!