Enzyme is an open source JavaScript testing utility by Airbnb that makes it fun and easy to write tests for React. In this article, we will be going through writing tests for React using Enzyme and Jest.
To get started, you will need to familiarize yourself with the following:
- React - A JavaScript library for building delightful UI by Facebook
- Jest - A JavaScript testing framework by Facebook.
Table of Contents
Why Jest?
Jest is a fast JavaScript testing utility by Facebook that enables you to get started with testing your JavaScript code with zero configuration.
This means that you can easily perform tasks like code-coverage by simply passing --coverage
option when running your tests.
Setting Up Our React App
First things first, create a react application using create-react-app.
create-react-app enzyme-tests
cd enzyme-tests
yarn start
Setting Up Enzyme
To get started with Enzyme, go ahead and install the library via yarn or npm as a dev dependency.
yarn add --dev enzyme enzyme-adapter-react-16
Notice that we also installed an adapter alongside enzyme that matches the React version installed by create-react-app.
If you are not using the latest version of React, Enzyme has adapters for all versions of react from ^0.13.0
to ^16.0.0
. You can read more about it in the Enzyme installation guide.
src/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;
Don't worry about the named exports from enzyme here. We will get to play with them later.
Finally, create a components
and components/__tests__
folder inside src
where our components and tests will live in respectively.
Shallow Rendering
Shallow rendering is the most basic version of testing with Enzyme. As the name suggests, shallow rendering limits it's scope to the component to be tested and not it's children.
This comes in handy in various scenarios:
- For presentational/dummy components that simply render props, there is no need to try and render any other children.
- For components with deeply nested children components, a change in behavior of the children should not affect the behavior of the parent component to be tested.
For this section, we will demonstrate testing a presentational component with shallow render.
Take a look at the List
component below that expects an items
prop and displays them in an unordered list.
src/components/List.js
import React from 'react';
import PropTypes from 'prop-types';
/_*
_ Render a list of items
_
_ @param {Object} props - List of items
*/
function List(props) {
const { items } = props;
if (!items.length) {
// No Items on the list, render an empty message
return <span className="empty-message">No items in list</span>;
}
return (
<ul className="list-items">
{items.map(item => <li key={item} className="item">{item}</li>)}
</ul>
);
}
List.propTypes = {
items: PropTypes.array,
};
List.defaultProps = {
items: [],
};
export default List;
Let's add a few tests for the component.
/src/components/tests/List.test.js
import React from 'react';
import { shallow } from '../enzyme';
import List from './List';
describe('List tests', () => {
it('renders list-items', () => {
const items = ['one', 'two', 'three'];
const wrapper = shallow(<List items={items} />);
// Expect the wrapper object to be defined
expect(wrapper.find('.list-items')).toBeDefined();
expect(wrapper.find('.item')).toHaveLength(items.length);
});
it('renders a list item', () => {
const items = ['Thor', 'Loki'];
const wrapper = shallow(<List items={items} />);
// Check if an element in the Component exists
expect(wrapper.contains(<li key='Thor' className="item">Thor</li >)).toBeTruthy();
});
it('renders correct text in item', () => {
const items = ['John', 'James', 'Luke'];
const wrapper = shallow(<List items={items} />);
//Expect the child of the first item to be an array
expect(wrapper.find('.item').get(0).props.children).toEqual('John');
});
});
The test suite imports a shallow
enzyme method from the configuration we created in the previous section, wraps the List
component and returns an instance of the rendered component.
We then make a series of assertions against the instance to check if the component renders the content correctly. While the tests are not optimal, we take advantage of a few methods exposed by the shallow API.
Full DOM rendering
in the last section, we were able to shallow render the List
component and write tests to assert the actual text in the li
tags. Sweet, right? In this section, we will look at full DOM rendering by making a few modifications to the component by breaking out the li
element into a component of it's own called ListItem
.
src/compoents/ListItem.js
import React from 'react';
import PropTypes from 'prop-types';
/_*
_ Render a single item
_
_ @param {Object} props
*/
function ListItem(props) {
const { item } = props;
return <li className="item">{item}</li>;
}
ListItem.propTypes = {
item: PropTypes.string,
};
export default ListItem;
With the new component, replace the li
tag with our shiny new component.
src/components/List.js
...
import ListItem from './ListItem';
...
return (
<ul className="list-items">
{items.map(item => <ListItem key={item} item={item} />)}
</ul>
);
Let's now run the tests we wrote in the previous section and see what happens. If you did this right, your tests should be failing as terribly as mine are.
Why would this happen? I mean the UI did not change at all. All we did was move things a littlte. Let's debug this further. The enzyme wrapper exposes a debug
method that allows use to peek into the wrapped instance of our component and see what went wrong.
Let's add a log in our tests to see what went wrong.
/src/components/tests/List.test.js
...
it('renders list-items', () => {
const items = ['one', 'two', 'three'];
const wrapper = shallow(<List items={items} />);
// Let's check what wrong in our instance
console.log(wrapper.debug());
// Expect the wrapper object to be defined
expect(wrapper.find('.list-items')).toBeDefined();
expect(wrapper.find('.item')).toHaveLength(items.length);
});
...
Run the tests again and look through the terminal output, you should see our component instance log.
As you can see, the wrapper method does not render the ListItem
Children as we would have expected. Therefore, our tests that checked for a class or li
element failed.
It may not seem necessary to shallow render such a simple component where the child is a presentational component but it comes in handy when you are writting tests for components that are wrapped by libraries such as react-redux's connect
or reduxForm
.
The idea here is that we do not want to test the inner workings of such high order components, therefore, no need to concern ourselves with their rendering.
Okay enough chatter. Let's fix the failling tests. We could stop checking for the li
elements in our tests and check for the ListItem
tag as shown below
/src/components/tests/List.test.js
...
it('renders list-items', () => {
const items = ['one', 'two', 'three'];
const wrapper = shallow(<List items={items} />);
// Expect the wrapper object to be defined
expect(wrapper.find('ListItem')).toBeDefined();
expect(wrapper.find('ListItem')).toHaveLength(items.length);
});
...
In this case, we actually want to test the entire tree of children in the List component. so instead, we will replace the shallow
component with mount
. Mount enables us to perform a full render. Here is a snippet of the updated code and a log of the debug instance.
/src/components/tests/List.test.js
import React from 'react';
import { mount } from '../enzyme';
import List from './List';
describe('List tests', () => {
it('renders list-items', () => {
const items = ['one', 'two', 'three'];
// Replace shallow iwth mount
const wrapper = mount(<List items={items} />);
// Let's check what wrong in our instance
console.log(wrapper.debug());
// Expect the wrapper object to be defined
expect(wrapper.find('.list-items')).toBeDefined();
expect(wrapper.find('.item')).toHaveLength(items.length);
});
...
});
As you can see, mount
's full rendering API renders the entire DOM, including that of the children. And our tests are fixed!
Note: unlike shallow or static rendering, full rendering actually mounts the component in the DOM, which means that tests can affect each other if they are all using the same DOM. Keep that in mind while writing your tests and, if necessary, use .unmount() or something similar as cleanup.
Enzyme Documentation
Static Rendering
Statics rendering works in the same way as shallow
and mount
but instead of returning an instance of the rendered output, it returns the rendered HMTL. It is built on top of Cheerio, a DOM manipulation and traversal API that borrows the magic of jQuery and strips out everything that we hate about it.
For static rendering, you do not have access to Enzyme
API methods such as contains
and debug
. You however have access to the full arsenal of Cheerios manipulation and traversal methods such as addClass
and find
respectively.
To statically render a React component, import the render method as shown in the snippet below.
/src/components/tests/List.test.js
import React from 'react';
import { render } from '../enzyme';
import List from './List';
import { wrap } from 'module';
describe('List tests', () => {
it('renders list-items', () => {
const items = ['one', 'two', 'three'];
const wrapper = render(<List items={items} />);
wrapper.addClass('foo');
// Expect the wrapper object to be defined
expect(wrapper.find('.list-items')).toBeDefined();
expect(wrapper.find('.item')).toHaveLength(items.length);
});
...
});
Conclusion
In this article, we have been able to go through the different ways of using Jest and Enzyme to test React components. While we did not dive into the specific methods exposed by the different renders, we were able to appreciate Enzyme's offerings.
The Enzyme documentation is very well done and should be easy for you to take advantage of the different methods. Next we will take a look at testing events, props and state in React. Get testing!
Like this article? Follow @_bharat_soni on Twitter