Testing is an important step in web application development, especially when it comes to apps made on top of component libraries like React. Among the many different tools and libraries for testing, Enzyme stands out for its flexibility and its easy-to-develop tests for your React code.
Actually, Enzyme is more of a JavaScript testing utility that makes it easier to assert, manipulate, and traverse your React components’ output. Created by Airbnb, it adds a lot of utility functions to facilitate the component rendering, finding and interacting with elements in a way that feels similar to jQuery.
In this tutorial, we’ll explore the different rendering options the utility provides, along with some practical examples.
First of all, make sure you have Node ≥ v8.10.0 on your local development machine. If you’re not sure whether you’re using the latest version, refer to nvm documentation to confirm.
For this project, we’re going to use create-react-app to set up a React application and get it running in no time. With it, we don’t need to install or configure tools like Webpack or Babel; they are preconfigured and hidden so that we can focus on the code.
It makes use of npx, an npm package runner that comes with npm ≥v5.2, so the whole thing is pretty straightforward. You can read more on npx here. 🙂
So, in order to create our application, run the following command in your prompt:
cd your-project-directory npx create-react-app logrocket-react-enzyme-app
This will create and initialize the project with a bunch of preset Node modules that are important for React development:
And this is the generated file and directories structure:
logrocket-react-enzyme-app ├── README.md ├── node_modules ├── package.json ├── .gitignore ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json └── src ├── App.css ├── App.js ├── App.test.js ├── index.css ├── index.js ├── logo.svg └── serviceWorker.js
Take a moment to open the package.json
file and analyze the pre-installed Node dependencies, including React, React DOM, and scripts; the preset scripts for starting up; building testing; and so on:
{ "name": "logrocket-react-enzyme-app", "version": "0.1.0", "private": true, "dependencies": { "react": "^16.8.6", "react-dom": "^16.8.6", "react-scripts": "3.0.0" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" }, "eslintConfig": { "extends": "react-app" }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] } }
To check out the running application, just type the npm start
command into the application root folder and the index page will be opened in your browser at http://localhost:3000/
just like that:
Now, let’s move on to the Enzyme setup. Once we’re using React 16 (the latest as of writing) for our project development, the following command may be issued to install Enzyme properly:
npm i — save-dev enzyme enzyme-adapter-react-16
After that, you’ll notice that our package.json
file was updated:
"devDependencies": { "enzyme": "^3.9.0", "enzyme-adapter-react-16": "^1.12.1" }
If you’re using a different version of React, no worries — you can follow the official Enzyme installation doc for guidance on how to install the right version for your project.
Note: if you’re using Linux and you get the error Error: ENOSPC: System limit for number of file watchers reached
, it means your system’s file watchers limit was hit. To fix it, simply run the following command:
echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p
In order to use Enzyme’s features, we need to tell our React app it is installed and available. However, remember that we need to reference the adapter package we installed earlier and properly set up the adapter to be used by Enzyme. So, go to the src
folder and create a new file called enzyme.js
:
import Enzyme, { configure, shallow, mount, render } from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; configure({ adapter: new Adapter() }); export { shallow, mount, render }; export default Enzyme;
This will be enough to export all the different Enzyme render types. Then, create another folder, tests
, into the src/components
directory to save our test files.
Let’s consider a simple example: an HTML ordered list. It’ll be composed by two components:
OrderedList
: this will store the function to build an <ol>
of OrderedListOption
elements, checking whether the list is empty or not for different display dispositionsOrderedListOption
: this deals with each <li>
item independently, CSS class, and its valueThis component could be easily adapted to a <select>
, for example, given its list nature. Let’s take a look at the source code:
import React from 'react'; import PropTypes from 'prop-types'; import OrderedListOption from './OrderedListOption'; function OrderedList(props) { const { options } = props; if (!options.length) { return <span className="empty">No options added.</span>; } return ( <ol className="options"> {options.map(option => <OrderedListOption key={option} value={option} />)} </ol> ); } OrderedList.propTypes = { options: PropTypes.array, }; OrderedList.defaultProps = { options: [], }; export default OrderedList;
The code is way simple: we’re importing React
and the OrderedListOption
component at the top of the file while checking for items’ array emptiness for displaying the proper HTML content. If the array is not empty, we’ll iterate over it to compose list options.
Take a look at the OrderedListOption
code:
import React from 'react'; import PropTypes from 'prop-types'; function OrderedListOption(props) { const { value } = props; return <li className="value">{value}</li>; } OrderedListOption.propTypes = { value: PropTypes.string, }; export default OrderedListOption;
It just receives the value of the element and places it into the <li>
element. Simple, isn’t it?
Before we create our tests, it’s important to state the three different render types Enzyme supports.
shallow
This basically renders a single component each time. In other words, Enzyme won’t consider the child elements for the test. Consider situations where you’d like to test the component itself isolated from the others around or inside of it. This render type is useful when you prefer unit testing rather than a full integrated test.
mount
This is the opposite of shallow
, working with the full DOM rendering, which includes all the child elements. It’s ideal for situations where each component interacts intensively with the others — the DOM API.
render
It renders to static HTML. This includes all the child elements. At the same time, it prevents access to React lifecycle methods, which, in turn, provides less flexibility and functionalities for testing — besides, it’s much faster. It is built on top of Cheerio, a DOM manipulation and traversal API based on jQuery Core for the server. So, you’ll have all the power of jQuery in your hands.
Now let’s make some tests. Create a new file called OrderedList.test.js
in our /tests
folder and add the following code:
import React from 'react'; import { shallow, mount, render } from '../../enzyme'; import OrderedList from '../OrderedList'; describe('Our test suite', () => { it('renders all the mocked animal options', () => { const animals = ['duck', 'bear', 'whale']; const wrapper = render(<OrderedList options={animals} />); expect(wrapper.find('.options')).toBeDefined(); expect(wrapper.find('.value')).toHaveLength(animals.length); }); it('renders no animal options', () => { const animals = []; const wrapper = shallow(<OrderedList options={animals} />); expect(wrapper.find('.empty').exists()).toBe(true); }); it('renders a single animal option', () => { const animals = ['duck']; const wrapper = mount(<OrderedList options={animals} />); expect(wrapper.contains(<li key='duck' className="value">duck</li >)).toBeTruthy(); }); it('renders correct text in animal option', () => { const animals = ['duck', 'bear', 'whale']; const wrapper = mount(<OrderedList options={animals} />); expect(wrapper.find('.value').get(0).props.children).toEqual('duck'); }); });
First, we’re importing the three render types in the beginning of the file from the enzyme.js
file we created before. Here, we’re contemplating four test scenarios for each type of rendering.
The first one is for the render
type; we’re basically asking Enzyme to render an OrderedList
with the given array of animals param and asserting the test conditions through the expect()
function.
The wrapper
object represents the render()
result, and within it, we can call to find
the CSS classes options
(of our items’ children) and value
, regarding each of the inner list elements’ classes. We’re also testing the number of child elements.
The second test focuses on a list that receives no elements. Here, we’re using the shallow
render type, which makes methods like exists()
available to us.
The last two tests make use of the mount()
function, which will return the full DOM to the wrapper
object. The method contains()
is another example of a React lifecycle method.
You can go even further and test things like forms, form elements, events, and so on. Let’s take a look at a second example, a login form component (Login.js
):
import React from 'react'; class Login extends React.Component { constructor() { super() this.state = { username: '', password: '' } } handleInputChange = (event) => { this.setState({ [event.target.name]: event.target.value }) } render() { return ( <form className='login'> <label>Username</label> <input id='email' onBlur={this.handleInputChange} name='email' type='text' /> <label>Password</label> <input id='password' onBlur={this.handleInputChange} name='password' type='password' /> <button>Submit</button> </form> ) } } export default Login
It’s a common form structure component, except that the state we’re keeping here must be updated every time a blur event occurs in one of the inputs.
Let’s take a look at the Login.test.js
file:
import React from 'react'; import { shallow, mount, render } from '../../enzyme'; import Login from '../Login' describe('Login Test Suite', () => { it('should render the form', () => { const wrapper = shallow(<Login />); expect(wrapper.find('form.login').exists()).toBe(true); expect(wrapper.find('#email').length).toEqual(1); expect(wrapper.find('#password').length).toEqual(1); }) }) describe('Email Test Suite', () => { it('should change the state of the Login component', () => { const wrapper = shallow(<Login />); wrapper.find('#email').simulate('blur', { target: { name: 'email', value: '[email protected]' } }); expect(wrapper.state('email')).toEqual('[email protected]'); }) }) describe('Password Test Suite', () => { it('should change the state of the Login component', () => { const wrapper = mount(<Login />); wrapper.find('#password').simulate('blur', { target: { name: 'password', value: 'my log is rocket' } }); expect(wrapper.state('password')).toEqual('my log is rocket'); }) })
The first test suite is nothing new; we’re just checking if the form elements exist. The second and third tests are making use of the simulate()
function to, as the name suggests, simulate an event in the field — in this case, onBlur
.
Once we’ve set that the onBlur
will trigger the state update of each input field, we can then check if the same state was stored. That’s a great example of a behavior test, wherein we’re testing what happens after Enzyme forces the simulation of an event in the component.
This would be the final output in your IDE (here, Visual Studio Code):
You can access the full source code from my GitHub repo. If you enjoyed it, please leave a star rating.
We presented here only a few methods and examples among the many others Enzyme provides. It is a dynamic and rich environment for you to create your test suites and explore many different testing scenarios, such as for integration, unitary, behavioral, semantic, and others.
Refer to the official documentation page for more information about Enzyme and its functions, additional configurations, and more. Aim for a well-tested application and make it more trustworthy.
Have you used Enzyme for React? Tell us a bit about the experience…
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowIn this article, you’ll learn how to set up Hoppscotch and which APIs to test it with. Then we’ll discuss alternatives: OpenAPI DevTools and Postman.
Learn to migrate from react-native-camera to VisionCamera, manage permissions, optimize performance, and implement advanced features.
SOLID principles help us keep code flexible. In this article, we’ll examine all of those principles and their implementation using JavaScript.
JavaScript’s Date API has many limitations. Explore alternative libraries like Moment.js, date-fns, and the new Temporal API.