<blockquote>Disclaimer: due to the amount of information, this article is divided in three parts. The first part consists of a small intro to React testing, which includes configuration and snapshot/state changes tests. The second part shows how to test components using mocks and the third part brings more complex test cases with components that are using <a href="https://redux.js.org">redux</a>.</blockquote><p>Testing your project is an important way to assure your (and your team's) trust towards the code you wrote. At <a href="https://www.vinta.com.br">Vinta</a>, we are very familiar with backend testing and doing so in our Django projects is a mandatory part of our development process; with open source tools like <code>pytest</code>, <code>coverage</code>, <code>mommy</code> and many others, we have a very concrete and clear way of conducting our tests. But what about the frontend? With the growing popularity of some libraries and frameworks like <a href="https://reactjs.org">React</a>, <a href="https://vuejs.org">Vue</a> and <a href="https://angular.io">Angular</a>, JavaScript has grown in complexity and it's no longer considered as just a part of the static files in the project's settings, but as the base language of a separate project that is as significant as the backend - and just like the backend, it'll also require good testing practices.</p><p>Vinta has been using <code>React</code> in its projects for quite a while now and, as they grow, the necessity of writing code that is as bug free as possible also grows. This article consists of a compilation of what I've learned while writing tests. Keep in mind that what you'll read here is <em>purely my opinion on the matter</em> and it's based mostly on experiences I've had (and some research).</p><h1 id="packages-and-configuration">Packages and configuration</h1><p>The first thing you have to do is choose a test framework. Since this post focus on React, we'll be using <code>jest</code> alongside with <code>enzyme</code> in all of the examples. Below you'll find a list of all the packages that will be used (if you're using our <a href="https://github.com/vintasoftware/django-react-boilerplate">boilerplate</a>, they will already be shipped with it). There's also a repository containing the base code used to write this article <a href="https://github.com/victorfsf/jest-enzyme-examples">here</a>.</p><ul><li>jest (<a href="https://github.com/facebook/jest">repository</a>) (<a href="https://jestjs.io">documentation</a>) - In case you get overwhelmed over jest's documentation, you can also check out the <a href="https://github.com/sapegin/jest-cheat-sheet">jest-cheat-sheet repository</a></li><li>jest-css-modules (<a href="https://github.com/justinsisley/Jest-CSS-Modules">repository</a>)</li><li>identity-obj-proxy (<a href="https://github.com/keyanzhang/identity-obj-proxy">repository</a>) - Not needed if you're not using <code>css-modules</code></li><li>enzyme (<a>repository</a>) (<a href="https://airbnb.io/enzyme/">documentation</a>)</li><li>enzyme-to-json (<a href="https://github.com/adriantoine/enzyme-to-json">repository</a>)</li><li>enzyme-adapter-react - This package will depend on the React version you're using. If you're using the most recent version (as of now, it's 16.x), install <a href="https://www.npmjs.com/package/enzyme-adapter-react-16">enzyme-adapter-react-16</a>. Other versions can be found on <a href="https://github.com/airbnb/enzyme">enzyme's repository</a></li></ul><p>Once everything is installed, we'll need to configure our <code>package.json</code> file (this is also configured on our <a href="https://github.com/vintasoftware/django-react-boilerplate">boilerplate</a> already):</p><pre><code class="language-javascript">{ ..., // The rest of your configuration "jest": { "testURL": "http://localhost/", "transform": { ".*": "<rootDir>/node_modules/jest-css-modules" }, "moduleNameMapper": { "^.+\\.(css|scss)$": "identity-obj-proxy" }, "transformIgnorePatterns": [ "node_modules/*" ], "modulePaths": [ "." ], "snapshotSerializers": [ "enzyme-to-json/serializer" ], "setupFiles": [ "./test/jest-setup.js" ] }, "scripts": { ..., // All of your other scripts "test": "jest" }, ... // The rest of your configuration
}
</code></pre><p>The snippet above describes the minimum configuration needed to have an optimal testing experience with jest/enzyme. All of those keys are very well explained on <a href="https://jest-bot.github.io/jest/docs/configuration.html">jest's config documentation</a> (you should take sometime to read it). Once your <code>package.json</code> file is configured, just run <code>npm run test</code> and jest will execute all of the tests for you.</p><p>Below you can find the use case of some packages and where they were used on the <code>package.json</code> file.</p><h3 id="transform">transform</h3><p>The <code>jest-css-modules</code> package goes here. All it does is to prevent CSS parse errors when jest runs your code (as described in its documentation).</p><h3 id="modulenamemapper">moduleNameMapper</h3><p><code>identity-obj-proxy</code> is a package that mocks webpack imports (such as <a href="https://github.com/gajus/babel-plugin-react-css-modules">CSS modules</a>), so there won't be any problems during snapshot tests. In this post's case, it basically converts <code>className={styles.myClassName}</code> to <code>className='myClassName'</code>, instead of generating an unique name for that class. If you’re not using CSS modules, you probably won’t need this package.</p><h3 id="snapshotserializers">snapshotSerializers</h3><p>The <code>enzyme-to-json</code> package is a serializer that converts your enzyme wrappers to a format that is compatible with jest's snapshots.</p><h1 id="setting-up-a-jest-setup-file">Setting up a jest setup file</h1><p>When using enzyme, the jest setup file is a must have. This file is responsible for preloading code when running jest (you can also set some variables as global variables). As you can see below, I've loaded the enzyme adapter for my react version, which will run everytime I execute the jest command.</p><pre><code class="language-javascript">import Adapter from 'enzyme-adapter-react-16';
import enzyme from 'enzyme'; enzyme.configure({ adapter: new Adapter() });
</code></pre><h1 id="rendering-components-with-enzyme">Rendering components with enzyme</h1><p>When it comes to testing your components, it's best to use enzyme, as it comes with a great <a href="https://airbnb.io/enzyme/">API to <em>assert, manipulate, and traverse your component's output</em></a>. The code below shows three test cases that you can work with when using enzyme to render your component. There are a few differences between them, and you can check them out in details <a href="https://gist.github.com/fokusferit/e4558d384e4e9cab95d04e5f35d4f913">on this gist</a>. Although all you need to know to begin with is this:</p><ul><li>You should always start with <code>shallow</code>. It doesn’t render the full component tree, so it’s lighter and allows you to build more isolated tests.</li><li>Unless you're testing your component's lifecycle methods or need to fully render its children. In these cases, use <code>mount</code>.</li><li>If you don't care about the lifecycle methods, but wants to render all children, use <code>render</code>.</li><li>Always remember that <code>shallow</code> will not render child components (but will render simple HTML elements).</li></ul><pre><code class="language-javascript">import { mount, shallow, render } from 'enzyme'; describe('Suite description', () => { test('mount case', () => { const wrapper = mount(<Component />); }); test('shallow case', () => { const wrapper = shallow(<Component />); }); test('render case', () => { const wrapper = render(<Component />); });
});
</code></pre><h1 id="testing-with-snapshots">Testing with snapshots</h1><p>Snapshot testing is an easy and practical way to test your component's structure, as long as you keep things simple. When you do this type of test, a snapshot of your component is generated; this can be used to check if things are rendering the way you expected them to.</p><p>If you run <code>npm run test</code> when testing with snapshots, it'll take one of the three possible actions for each test:</p><ul><li>If there isn't a snapshot generated for that test, generate a new one.</li><li>If there is a snapshot, generate a new one based on the current component and compare it with the older snapshot. If they are the same, the test passes; otherwise, it fails.</li><li>If you run <code>npm run test -- -u</code> or <code>jest -u</code>, it'll generate new snapshots and replace the old ones, if necessary.</li></ul><h3 id="when-to-use-snapshots">When to use snapshots</h3><p>Snapshots shouldn't be used to test every component your project has. Some components are more fit than others to be tested like this and the ones that are, are usually simple. So, when you're deciding whether you should use snapshots or not, consider the following points:</p><ul><li>Only use snapshots if you <strong>intentionally</strong> intended to do so. If you find yourself asking the question <em>"Should I do a snapshot test for this?"</em>, the answer is probably <strong>no, you shouldn't</strong>.</li><li>The best case for snapshots is when the component is presentational (it doesn't deal with state changes).</li><li>Don't use snapshots if your component's code is too big. Big snapshots makes it hard to validate them (it also creates a huge file) and will be a pain to maintain later.</li><li><strong>Never</strong> mix <code>mount</code> or <code>render</code> with snapshots. The reason for this is that you might end up rendering big child components, which will result in an even bigger snapshot, and also <strong>you shouldn't care about what happens inside other components when doing snapshot tests</strong>, since this method should be used to test a <strong>single component's state only</strong>.</li></ul><p>[newsletter widget]</p><h3 id="simple-rendering">Simple rendering</h3><p>The code below describes a simple <code>ButtonGroup</code> component with no state changes, no props, no external children and three buttons where the first one is active. This is a very simple case, which is perfect for snapshots.</p><pre><code class="language-javascript">// File: ButtonGroup.js
import React from 'react';
import styles from './style.scss'; const ButtonGroup = () => ( <div className={styles.buttonGroup}> <button type='button' className={styles.active}>Option 1</button> <button type='button'>Option 2</button> <button type='button'>Option 3</button> </div>
);
</code></pre><pre><code class="language-javascript">// File: ButtonGroup.spec.js
import { shallow } from 'enzyme';
import ButtonGroup from './ButtonGroup'; describe('ButtonGroup', () => { test('it renders three buttons with the first one active', () => { const wrapper = shallow(<ButtonGroup />); expect(wrapper).toMatchSnapshot(); });
});
</code></pre><p>Since there are no lifecycles to test and no components as children that we want to render, <code>shallow</code> is the best option here. The <code>.snap</code> file will look like this:</p><pre><code class="language-javascript">// File: __snapshots__/ButtonGroup.spec.js.snap
exports[`it renders three buttons with the first one active 1`] = `
<div className='buttonGroup'
> <button type='button' className='active' > Option 1 </button> <button type='button' > Option 2 </button> <button type='button' > Option 3 </button>
</div>
`;
</code></pre><p>You can also use snapshots to test a small part of your component by using the wrapper's <code>.find</code> method. As you can see below, doing so will render only the button that has <code>className='active'</code>:</p><pre><code class="language-javascript">// File: ButtonGroup.spec.js
import { shallow } from 'enzyme';
import ButtonGroup from './ButtonGroup'; describe('ButtonGroup', () => { test('it renders the first button as active', () => { const wrapper = shallow(<ButtonGroup />); expect(wrapper.find('.active')).toMatchSnapshot(); });
});
</code></pre><pre><code class="language-javascript">// File: __snapshots__/ButtonGroup.spec.js.snap
exports[`it renders the first button as active 1`] = `
<button type='button' className='active'
> Option 1
</button>
`;
</code></pre><p>This can be used when you want to check for changes in specific parts of a component and want the snapshot to render only what's affected by them.</p><h3 id="conditional-rendering">Conditional rendering</h3><p>Ideally, you shouldn't use snapshots to test for state and prop changes; but if those changes are simple, they can actually be a good choice. Below we have a <code>Tag</code> component which will render a <code>TagForm</code> if clicked. When doing snapshot tests for this case, it's important to know how it should behave in every possible state and prop changes. If you can make a list to keep track of every possible case, do it - this may also help you decide whether you should use snapshots or if you should break your component down to smaller parts, in case you end up with a big component with a lot of possibilities.</p><pre><code class="language-javascript">// File: Tag.js
import React, { Component } from 'react';
import TagForm from './TagForm';
import styles from './style.scss'; class Tag extends Component { constructor (props) { super(props); this.state = { isFormOpen: false, } } handleClick () { this.setState({ isFormOpen: !this.state.isFormOpen, }); } render () { const { name } = this.props; const { isFormOpen } = this.state; return isFormOpen ? ( <TagForm initialValues=Testing your React components (part 1) onCancel={() => this.handleClick()} /> ) : ( <div className={styles.tag} onClick={() => this.handleClick()}> {name} </div> ); }
}
</code></pre><pre><code class="language-javascript">// File: TagForm.js
import React, { Component } from 'react';
import styles from './style.scss'; class TagForm extends Component { // Ignore the form's logic, it's not relevant here handleSubmit (e) { // Submit logic } handleChange (e) { // Change logic } handleBlur (e) { // Blur logic } render () { const { initialValues, onCancel } = this.props; return ( <form className={styles.tagForm}> <input type='text' id='name' value={initialValues.name} onChange={this.handleChange} onBlur={this.handleBlur} /> <div className={styles.buttons}> <button type='submit' onClick={this.handleSubmit} className={styles.submit} /> <button type='button' onClick={onCancel} className={styles.cancel} /> </div> </form> ); }
}
</code></pre><p>The following code shows how the tests are written.</p><pre><code class="language-javascript">// File: Tag.spec.js
import { shallow } from 'enzyme';
import Tag from './Tag'; describe('Tag', () => { test('it renders a tag named "test"', () => { const wrapper = shallow(<Tag name='test' />); expect(wrapper).toMatchSnapshot(); }); test('it renders a TagForm when the tag is clicked', () => { const wrapper = shallow(<Tag name='test' />); wrapper.find('.tag').simulate('click'); expect(wrapper).toMatchSnapshot(); }); });
</code></pre><pre><code class="language-javascript">// File: __snapshots__/Tag.spec.js.snap
exports[`it renders a tag named "test" 1`] = `
<div className='tag' onClick={[Function]}
> test
</div>
`; exports[`it renders a TagForm when the tag is clicked 1`] = `
<TagForm initialValues={ Object { "name": "test" } } onCancel={[Function]}
/>
`;
</code></pre><p>As you can see, the <code>TagForm</code> component was not really rendered, thanks to the <code>shallow</code> renderer. This prevented the snapshot from cascading every component inside <code>TagForm</code>, keeping the test clean. This is what would've happened if <code>mount</code> was used:</p><pre><code class="language-javascript">// File: __snapshots__/Tag.spec.js.snap
... exports[`it renders a TagForm when the tag is clicked 1`] = `
<Tag name='test'
> <TagForm initialValues={ Object { "name": "test" } } onCancel={[Function]} /> <form className='tagForm' > <input type='text' id='name' value='test' onChange={[Function]} onBlur={Function]} /> <div className='buttons' > <button type='submit' onClick={[Function]} className='submit' /> <button type='button' onClick={{Function]} className='cancel' /> </div> </form> </TagForm>
</Tag>
`;
</code></pre><p><code>TagForm</code> isn't a really big component, but it also didn't need to be fully rendered, since you shouldn't care what happens inside it in this moment.</p><h1 id="testing-the-component-s-flow">Testing the component's flow</h1><p>Thanks to enzyme, testing the component's flow has become quite simple. This is where <code>mount</code> and <code>render</code> shine and where you'll use enzyme's API methods the most. This approach is useful to test:</p><ul><li>Integrations between a parent and its children components.</li><li>UI interactions such as clicks, changes and form submissions.</li><li>Direct reactions to state and prop changes.</li></ul><p>As you can see on <a>examples above</a>, this can also be used alongside snapshots to test how your component renders when there are state changes. The example below will use the same <code>Tag</code> and <code>TagForm</code> components as before to demonstrate some test cases using the enzyme API.</p><pre><code class="language-javascript">import { shallow } from 'enzyme';
import Tag from './Tag'; describe('Tag', () => { test('it renders a TagForm when the tag is clicked', () => { const wrapper = shallow(<Tag name='test' />); wrapper.find('.tag').simulate('click'); // Renders the form expect(wrapper.find('TagForm')).toHaveLength(1); // Doesn't render the tag anymore expect(wrapper.find('.tag')).toHaveLength(0); }); test('it renders a Tag when cancel is clicked', () => { const wrapper = shallow(<Tag name='test' />); // Opens the form wrapper.find('.tag').simulate('click'); // Click on cancel, wrapper.find('button').at(1).simulate('click'); // which should return to the tag's view mode expect(wrapper.find('.tag')).toHaveLength(1); });
});
</code></pre><p>That's it for today! Here's a recap on what we've learned:</p><ul><li>Always ask yourself whether you should do snapshot tests before you actually write them.</li><li>Snapshot files are generated automatically if they don't exist when you run your tests.</li><li>Keep your test cases simple, split your component (or the test cases) into smaller parts if you can.</li><li>Don't use snapshots if your component's code is too big, things can get confusing.</li><li>The best case for snapshots is when the component is presentational.</li><li>Only test snapshots using <code>shallow</code>, since you shouldn't care about what happens inside other components when doing snapshot tests.</li><li>Be very careful when choosing which rendering method you'll choose.</li><li><code>wrapper.find</code> can find everything from class names to component names. It works like <code>jQuery</code>'s <code>find</code> method.</li><li>You should definitely read both <a href="https://jestjs.io/docs/en/api">Jest's</a> and <a href="https://airbnb.io/enzyme/docs/api/">Enzyme's</a> API documentations. You can find a solution for most of your problems in them.</li></ul><h1 id="coming-next">Coming next</h1><p>The second part of this article includes more complex cases with tests using <code>mocks</code> and can be read <a href="https://www.vinta.com.br/blog/2019/testing-your-react-components-part-2/">here</a>.</p><p>Thanks to <a href="https://www.vinta.com.br/blog/author/arineto/">Arimatea Neto</a>, <a href="https://www.vinta.com.br/blog/author/hugobessa/">Hugo Bessa</a> and <a href="https://github.com/luizbraga">Luiz Braga</a> for reviewing this post!</p>
Join the Tech Forward newsletter
Stay ahead of the curve with our latest trends about web development.
By clicking “Accept all”, you agree to the storing of cookies on your device to enhance site navigation, analyze site usage and assist in our marketing efforts. Check our privacy policies.