w3resource

Testing React Apps


Jest is commonly used to test React applications

Setup

Setup using Create React App

In the case where you are just getting started with React, we recommend that you use Create React App. Create React App is ready to use and ships with Jest! You only need to add react-test-renderer for rendering snapshots.

Run

yarn add --dev react-test-renderer

Setup without Create React App

If you already have an existing application, you'll need to install a few packages to make everything work well together. We will be using the babel-jest package as well as the react babel preset to transform our code inside of the test environment.

Run

yarn add --dev jest babel-jest @babel/preset-env @babel/preset-react react-test-renderer

Your package.json needs to look something like this (where <current-version> is the actual latest version number for the package). It is advised that you add the scripts and jest configuration entries:

// package.json
"dependencies": {
    "react": "<current-version>",
    "react-dom": "<current-version>"
  },
  "devDependencies": {
    "@babel/preset-env": "<current-version>",
    "@babel/preset-react": "<current-version>",
    "babel-jest": "<current-version>",
    "jest": "<current-version>",
    "react-test-renderer": "<current-version>"
  },
  "scripts": {
    "test": "jest"
  }
// babel.config.js
module.exports = {
  presets: ['@babel/preset-env', '@babel/preset-react'],
};

And with that you are set.

Snapshot Testing

Let us create a snapshot test for a Link component that renders hyperlinks:

// Link.react.js
import React from 'react';

const STATUS = {
  HOVERED: 'hovered',
  NORMAL: 'normal',
};

export default class Link extends React.Component {
  constructor(props) {
    super(props);

    this._onMouseEnter = this._onMouseEnter.bind(this);
    this._onMouseLeave = this._onMouseLeave.bind(this);

    this.state = {
      class: STATUS.NORMAL,
    };
  }

  _onMouseEnter() {
    this.setState({class: STATUS.HOVERED});
  }

  _onMouseLeave() {
    this.setState({class: STATUS.NORMAL});
  }

  render() {
    return (
      <a
        className={this.state.class}
        href={this.props.page || '#'}
        onMouseEnter={this._onMouseEnter}
        onMouseLeave={this._onMouseLeave}
      >
        {this.props.children}
      </a>
    );
  }
}

Now let us use React's test renderer and the Jest's snapshot feature to interact with the component and then capture the rendered output and create a snapshot file:

// Link.react.test.js
import React from 'react';
import Link from '../Link.react';
import renderer from 'react-test-renderer';

test('Link changes the class when hovered', () => {
  const component = renderer.create(
    <Link page="http://www.facebook.com">Facebook</Link>,
  );
  let tree = component.toJSON();
  expect(tree).toMatchSnapshot();

  // manually triggers the callback
  tree.props.onMouseEnter();
  // re-rendering
  tree = component.toJSON();
  expect(tree).toMatchSnapshot();

  // manually triggers the callback
  tree.props.onMouseLeave();
  // re-rendering
  tree = component.toJSON();
  expect(tree).toMatchSnapshot();
});

Whenever you run yarn test or jest, this produces an output file like this:

// __tests__/__snapshots__/Link.react.test.js.snap
exports[`Link will change the class when hovered 1`] = `
<a
  className="normal"
  href="http://www.facebook.com"
  onMouseEnter={[Function]}
  onMouseLeave={[Function]}>
  Facebook
</a>
`;

exports[`Link will change the class when hovered 2`] = `
<a
  className="hovered"
  href="http://www.facebook.com"
  onMouseEnter={[Function]}
  onMouseLeave={[Function]}>
  Facebook
</a>
`;

exports[`Link will change the class when hovered 3`] = `
<a
  className="normal"
  href="http://www.facebook.com"
  onMouseEnter={[Function]}
  onMouseLeave={[Function]}>
  Facebook
</a>
`;

The next time that you run the tests, the rendered output is compared to the previously created snapshot. The snapshot has to be committed along code changes. When your snapshot test fails, you will need to inspect whether it is an intended or unintended change. If the change is expected, you can invoke Jest with jest -u which will overwrite the existing snapshot.

Snapshot Testing with Mocks, Enzyme and React 16

There is a caveat around snapshot testing when you are using Enzyme and React 16+. In the case where you mock out a module using the following style:

jest.mock('../SomeDirectory/SomeComponent', () => 'SomeComponent');

You will see warnings in the console:

Warning: <SomeComponent /> is using uppercase HTML. Always use lowercase HTML tags in React.

# Or:

Warning: The tag <SomeComponent> is unrecognized in this browser. If you want to render a React component, you should start its name with an uppercase letter.

React 16 will trigger these warnings, these warnings are due to how it checks element types, and the mocked module will fail these checks. Your options are:

  1. Render as text. This way you would not see the props that are passed to the mock component in the snapshot, but it is straightforward:
  2. jest.mock('./SomeComponent', () => () => 'SomeComponent');
  3. Render as a custom element. DOM "custom elements" are not checked for anything and should not fire warnings. These elements are lowercase and have a dash in the name.
  4. jest.mock('./Widget', () => () => <mock-widget />);
  5. Use react-test-renderer. The test renderer does not care about element types and will happily accept e.g. SomeComponent. You can check snapshots by using the test renderer, and then you check component behavior separately using Enzyme.
  6. Disable warnings all together (this should be done in your jest setup file):
  7. jest.mock('fbjs/lib/warning', () => require('fbjs/lib/emptyFunction'));

Normally, this should not be your option for useful warnings. However, there are some cases, for instance when testing react-native's components, where we are rendering react-native tags into the DOM and many warnings are irrelevant. Another option will be to swizzle the console.warn and suppress specific warnings.

DOM Testing

If you want to assert, and manipulate your rendered components you can use react-testing-library, Enzyme, or React's TestUtils. Below are two examples that use react-testing-library and Enzyme.

react-testing-library

You need to run yarn add --dev @testing-library/react to use react-testing-library.

Let us implement a simple checkbox which swaps between two labels:

// CheckboxWithLabel.js
import React from 'react';
export default class CheckboxWithLabel extends React.Component {
  constructor(props) {
    super(props);
    this.state = {isChecked: false};

    // binds manually because React class components don't auto-bind
    // http://facebook.github.io/react/blog/2015/01/27/react-v0.13.0-beta-1.html#autobinding
    this.onChange = this.onChange.bind(this);
  }

  onChange() {
    this.setState({isChecked: !this.state.isChecked});
  }

  render() {
    return (
      <label>
        <input
          type="checkbox"
          checked={this.state.isChecked}
          onChange={this.onChange}
        />
        {this.state.isChecked ? this.props.labelOn : this.props.labelOff}
      </label>
    );
  }
}
// __tests__/CheckboxWithLabel-test.js
import React from 'react';
import {cleanup, fireEvent, render} from '@testing-library/react';
import CheckboxWithLabel from '../CheckboxWithLabel';

// automatically unmounts and cleanup DOM after the test is finished.
afterEach(cleanup);

it('CheckboxWithLabel changes the text after click', () => {
  const {queryByLabelText, getByLabelText} = render(
    <CheckboxWithLabel labelOn="On" labelOff="Off" />,
  );

  expect(queryByLabelText(/off/i)).toBeTruthy();

  fireEvent.click(getByLabelText(/off/i));

  expect(queryByLabelText(/on/i)).toBeTruthy();
});

Enzyme

You will need to run yarn add --dev enzyme to use Enzyme. In the case where you are using a React version below 15.5.0, you have to also install react-addons-test-utils.

Let us rewrite the test from above using Enzyme instead of react-testing-library. We use the shallow renderer of Enzyme in this example.

// __tests__/CheckboxWithLabel-test.js
import React from 'react';
import {shallow} from 'enzyme';
import CheckboxWithLabel from '../CheckboxWithLabel';

test('CheckboxWithLabel changes the text after click', () => {
  // Renders a checkbox with label in the document
  const checkbox = shallow(<CheckboxWithLabel labelOn="On" labelOff="Off" />);

  expect(checkbox.text()).toEqual('Off');

  checkbox.find('input').simulate('change');

  expect(checkbox.text()).toEqual('On');
});

Custom transformers

In the case where you need more advanced functionality, you can also build you're a custom transformer. using babel-jest, an example using babel is shown below:

// custom-transformer.js
'use strict';

const {transform} = require('@babel/core');
const jestPreset = require('babel-preset-jest');

module.exports = {
  process(src, filename) {
    const result = transform(src, {
      filename,
      presets: [jestPreset],
    });

    return result ? result.code : src;
  },
};

Installing @babel/core and babel-preset-jest packages is a perquisite for this example to work.

And for this to work with Jest, you have to update your Jest configuration with this: "transform":

{"\\.js$": "path/to/custom-transformer.js"}.

If you want to build a transformer with babel support, you may also use babel-jest to compose one and then pass in your custom configuration options:

const babelJest = require('babel-jest');

module.exports = babelJest.createTransformer({
  presets: ['my-custom-preset'],
});


Inviting useful, relevant, well-written and unique guest posts