Blog

Ideas and insights from our team

Testing your React components (part 1)


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 brings more complex cases using mocks and testing components that are connected with redux. The third and last part shows how to test formik wrapped components.

Testing your project is an important way to assure your (and your team's) trust towards the code you wrote. At Vinta, 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 pytest, coverage, mommy 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 React, Vue and Angular, 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.

Vinta has been using React 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 purely my opinion on the matter and it's based mostly on experiences I've had (and some research).

Packages and configuration

The first thing you have to do is choose a test framework. Since this post focus on React, we'll be using jest alongside with enzyme in all of the examples. Below you'll find a list of all the packages that will be used (if you're using our boilerplate, they will already be shipped with it).

Once everything is installed, we'll need to configure our package.json file (this is also configured on our boilerplate already):

{
  ..., // 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
}

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 jest's config documentation (you should take sometime to read it). Once your package.json file is configured, just run npm run test and jest will execute all of the tests for you.

Below you can find the use case of some packages and where they were used on the package.json file.

transform

The jest-css-modules package goes here. All it does is to prevent CSS parse errors when jest runs your code (as described in its documentation).

moduleNameMapper

identity-obj-proxy is a package that mocks webpack imports (such as CSS modules), so there won't be any problems during snapshot tests. In this post's case, it basically converts className={styles.myClassName} to className='myClassName', instead of generating an unique name for that class. If you’re not using CSS modules, you probably won’t need this package.

snapshotSerializers

The enzyme-to-json package is a serializer that converts your enzyme wrappers to a format that is compatible with jest's snapshots.

Setting up a jest setup file

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.

import Adapter from 'enzyme-adapter-react-16';
import enzyme from 'enzyme';

enzyme.configure({ adapter: new Adapter() });

Rendering components with enzyme

When it comes to testing your components, it's best to use enzyme, as it comes with a great API to assert, manipulate, and traverse your component's output. 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 on this gist. Although all you need to know to begin with is this:

  • You should always start with shallow. It doesn’t render the full component tree, so it’s lighter and allows you to build more isolated tests.
  • Unless you're testing your component's lifecycle methods or need to fully render its children. In these cases, use mount.
  • If you don't care about the lifecycle methods, but wants to render all children, use render.
  • Always remember that shallow will not render child components (but will render simple HTML elements).
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 />);
  });
});

Testing with snapshots

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.

If you run npm run test when testing with snapshots, it'll take one of the three possible actions for each test:

  • If there isn't a snapshot generated for that test, generate a new one.
  • 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.
  • If you run npm run test -- -u or jest -u, it'll generate new snapshots and replace the old ones, if necessary.

When to use snapshots

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:

  • Only use snapshots if you intentionally intended to do so. If you find yourself asking the question "Should I do a snapshot test for this?", the answer is probably no, you shouldn't.
  • The best case for snapshots is when the component is presentational (it doesn't deal with state changes).
  • 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.
  • Never mix mount or render 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 you shouldn't care about what happens inside other components when doing snapshot tests, since this method should be used to test a single component's state only.

Simple rendering

The code below describes a simple ButtonGroup 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.

// 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>
);
// 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();
  });
});

Since there are no lifecycles to test and no components as children that we want to render, shallow is the best option here. The .snap file will look like this:

// 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>
`;

You can also use snapshots to test a small part of your component by using the wrapper's .find method. As you can see below, doing so will render only the button that has className='active':

// 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();
  });
});
// File: __snapshots__/ButtonGroup.spec.js.snap
exports[`it renders the first button as active 1`] = `
<button
  type='button'
  className='active'
>
  Option 1
</button>
`;

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.

Conditional rendering

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 Tag component which will render a TagForm 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.

// 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={{ name }}
        onCancel={() => this.handleClick()}
      />
    ) : (
      <div className={styles.tag} onClick={() => this.handleClick()}>
        {name}
      </div>
    );
  }
}
// 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>
    );
  }
}

The following code shows how the tests are written.

// 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();
  });

});
// 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]}
/>
`;

As you can see, the TagForm component was not really rendered, thanks to the shallow renderer. This prevented the snapshot from cascading every component inside TagForm, keeping the test clean. This is what would've happened if mount was used:

// 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>
`;

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

Testing the component's flow

Thanks to enzyme, testing the component's flow has become quite simple. This is where mount and render shine and where you'll use enzyme's API methods the most. This approach is useful to test:

  • Integrations between a parent and its children components.
  • UI interactions such as clicks, changes and form submissions.
  • Direct reactions to state and prop changes.

As you can see on examples above, 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 Tag and TagForm components as before to demonstrate some test cases using the enzyme API.

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);
  });
});

That's it for today! Here's a recap on what we've learned:

  • Always ask yourself whether you should do snapshot tests before you actually write them.
  • Snapshot files are generated automatically if they don't exist when you run your tests.
  • Keep your test cases simple, split your component (or the test cases) into smaller parts if you can.
  • Don't use snapshots if your component's code is too big, things can get confusing.
  • The best case for snapshots is when the component is presentational.
  • Only test snapshots using shallow, since you shouldn't care about what happens inside other components when doing snapshot tests.
  • Be very careful when choosing which rendering method you'll choose.
  • wrapper.find can find everything from class names to component names. It works like jQuery's find method.
  • You should definitely read both Jest's and Enzyme's API documentations. You can find a solution for most of your problems in them.

Coming next

The second part of this article will include more complex cases with tests using mocks, shallow with dive and components that use redux.

Thanks to Arimatea Neto, Hugo Bessa and Luiz Braga for reviewing this post!

About Victor Ferraz

Fullstack Developer

Comments