Diogo Souza Brazilian dev. Creator of www.altaluna.com.br

Getting started with Enzyme for React

7 min read 1964

Getting Started With Enzyme For React

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.

Setup and install

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:

Facebook create-react-app output.

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:

Index React App page

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

Creating our first test

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 dispositions
  • OrderedListOption: this deals with each <li> item independently, CSS class, and its value

This 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.

Testing form components

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: 'logrocket@mail.com' }
            });

        expect(wrapper.state('email')).toEqual('logrocket@mail.com');
    })
})

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):

Test results in the terminal

Conclusion

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…

Plug: , a DVR for web apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

.

Diogo Souza Brazilian dev. Creator of www.altaluna.com.br

Leave a Reply