React Unit Testing with Jest and Enzyme

create-react-app is a great tool that lets you setup a React development environment quickly, without having to first become an expert on Webpack, Babel, and all the other tools you will need to be effective. We like it.

create-react-app comes preconfigured with the Jest test runner which lets you test your components as you develop them. Jest has a lot of features, but you don't actually need most of them. You can get a lot of benefit from very little effort with just two types of tests: "Smoke" tests and snapshots.

Smoke Testing

A "smoke test" simply verifies that a component can load without throwing, but it is likely to catch many common errors. This is an example from the create-react-app user guide:

import React from 'react';  
import ReactDOM from 'react-dom';  
import App from './App';

it('renders without crashing', () => {  
  const div = document.createElement('div');
  ReactDOM.render(<App />, div);
});

A good place to start testing React components is to create smoke tests for all your components, building on this pattern. It requires little effort but will most likely help you catch errors at an earlier stage than would otherwise be the case.

Shallow Rendering

The code above verifies that the App component can be rendered into the DOM without crashing (using jsdom). However, since App usually has other React components as children you are implicitly testing those components also when you run the App smoke test above.

In some situations this may be what you want, but it can be very useful to restrict a test to the specific component that you are working on (tests complete faster, and you will not be distracted by failures in other components). This type of "shallow" testing can be done quite easily with the shallow rendering API from the enzyme package.

We can rewrite the previous test to use shallow like so:

import React from 'react';  
import { shallow } from 'enzyme';  
import App from './App';

it('renders without crashing', () => {  
  const wrapper = shallow(<App/>);
});

The difference between the two types of tests becomes more obvious when looking at their snapshots, so we'll take a look at those next.

Snapshot Testing

Testing user interface components can quickly become a tedious, time-consuming business of writing test after test that verifies that all the child elements are rendered correctly, that all classes are applied correctly, etc. In many cases you can avoid writing this type of tests through the use of Jest's snapshot feature.

Snapshot testing the App component looks like this:

import React from 'react';  
import renderer from 'react-test-renderer';  
import App from '../App';

it('matches its snapshot', () => {  
  const tree = renderer.create(<App/>).toJSON();
  expect(tree).toMatchSnapshot();
});

In the above code we are rendering the App component into a JavaScript object that we then serialise with toJSON(). Jest then stores the serialised output as a snapshot, and in subsequent test runs it verifies that the contents of the tree constant matches the stored snapshot.

A snapshot of our App component might look something like this:

<div  
  className="App"
>
  <header
    className="header"
  >
    <img
      alt="Credit: Jonathan Li from the Noun Project"
      className="header__logo"
      src="mandala.png"
    />
    <h2
      className="header__heading"
    >
      Sign up below for The Big Event
    </h2>
  </header>
  <div
    className="SignupForm"
  >
    <div
      className="SignupForm--signup"
    >
      <form
        noValidate={true}
        onSubmit={[Function]}
      >
        <div
          className="Input"
        >
          <label
            className="Input__label Input__label--required"
            htmlFor="firstName"
          >
            First Name
          </label>
          <input
            className="Input__input"
            name="firstName"
            onChange={[Function]}
            required={true}
            type="text"
            value=""
          />
        </div>
        <div
          className="Input"
        >
          <label
            className="Input__label Input__label--required"
            htmlFor="lastName"
          >
            Last Name
          </label>
          <input
            className="Input__input"
            name="lastName"
            onChange={[Function]}
            required={true}
            type="text"
            value=""
          />
        </div>
        <div
          className="Input"
        >
          <label
            className="Input__label Input__label--required"
            htmlFor="email"
          >
            Email Address
          </label>
          <input
            className="Input__input"
            name="email"
            onChange={[Function]}
            required={true}
            type="text"
            value=""
          />
        </div>
        <div
          className="Textarea"
        >
          <label
            className="Textarea__label"
            htmlFor="comments"
          >
            Comments
          </label>
          <textarea
            className="Textarea__textarea"
            name="comments"
            onChange={[Function]}
            required={undefined}
            value=""
          />
        </div>
        <div
          className="SubmitButton"
        >
          <input
            className="SubmitButton__input"
            disabled={true}
            type="submit"
            value="Sign up"
          />
        </div>
      </form>
      <div>

      </div>
    </div>
  </div>
</div>  

If a snapshot comparison fails, you know that something in the rendered component has changed. If this is unexpected, you can then check your code and correct any errors to make the test pass. If the change was intentional, you can tell Jest to update the stored snapshot (by pressing the "u" key in Jest's output window).

The output from Jest will highlight any mismatches between the rendered component and the snapshot, and because the snapshots are stored in a human-readable format it is usually very easy to determine the correct action to take when a snapshot test fails.

This has worked very well for us because our user interface components are rarely specified in great detail before we begin working on them. We usually start by creating React components that render static HTML, and once we are satisfied that we have a reasonable component hierarchy we begin adding state and behaviour. Snapshot testing allows us to start testing a component early on in the development process, without having to continuously rewrite tests that become obsolete because we change our minds about some implementation detail.

Snapshot Testing with Shallow

The snapshot that we created above contains rendered versions not only of our App component, but also renders of its children (and their children, and so on), so the test will fail if something changes any of those components.

We can use shallow to avoid rendering the children, but the shallow wrapper from Enzyme cannot be converted to a string using JSON.stringify or .toJSON(). Fortunately, there is an npm module called enzyme-to-json that we can use instead:

import React from 'react';  
import { shallow } from 'enzyme';  
import toJson from 'enzyme-to-json';  
import App from '../App';

it('matches its shallow snapshot', () => {  
  const wrapper = shallow(<App />);
  expect(toJson(wrapper)).toMatchSnapshot();
});

This is what the snapshot looks like when we render our App component using shallow:

<div  
  className="App"
>
  <header
    className="header"
  >
    <img
      alt="Credit: Jonathan Li from the Noun Project"
      className="header__logo"
      src="mandala.png"
    />
    <h2
      className="header__heading"
    >
      Sign up below for The Big Event
    </h2>
  </header>
  <SignupForm />
</div>  

Notice that SignupForm and its child components are not being rendered. This makes our snapshot much easier to work with.

Shallow or Full Render?

We tend to use shallow rendering more often in tests because of the simplicity and speed. One limitation of this approach is that shallow rendering does not invoke the full React component lifecycle, so if your test depends on something that happens when componentDidMount or componentDidUpdate is being called you will need to use a full DOM render in your test.

If you are new to testing React components, our suggestion would be to begin with shallow rendering and see how far it takes you, and then move to full rendering when you find you need it.

Unit testing of user interfaces has had a reputation of being hard, to the extent that many have found it simply wasn't worth the effort. But our experience with the test tools described in this article has been that they quickly became an aid in the process of writing code instead of a hindrance. If you have tried before and given up, maybe it is time to try again.