Testing React Apps
At Facebook, we use Jest to test React applications.
Setup #
If you are just getting started with React, we recommend using create-react-app. It is ready to use and ships with Jest!
If you have an existing application you'll need to install a few packages to make everything work well together. We are using the babel-jest
package and the react
babel preset to transform our code inside of the test environment. Also see babel integration.
Run
npm install --save-dev jest babel-jest babel-preset-es2015 babel-preset-react react-test-renderer
Your package.json
should look something like this (where <current-version>
is the actual latest version number for the package). Please add the scripts and jest configuration entries:
// package.json "dependencies": { "react": "<current-version>", "react-dom": "<current-version>" }, "devDependencies": { "babel-jest": "<current-version>", "babel-preset-es2015": "<current-version>", "babel-preset-react": "<current-version>", "jest": "<current-version>", "react-test-renderer": "<current-version>" }, "scripts": { "test": "jest" }
// .babelrc { "presets": ["es2015", "react"] }
And you're good to go!
Snapshot Testing #
Let's create a snapshot test for a Link component that renders hyperlinks:
// Link.react.js import React from 'react'; const STATUS = { NORMAL: 'normal', HOVERED: 'hovered', }; 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's use React's test renderer and Jest's snapshot feature to interact with the component and 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 trigger the callback tree.props.onMouseEnter(); // re-rendering tree = component.toJSON(); expect(tree).toMatchSnapshot(); // manually trigger the callback tree.props.onMouseLeave(); // re-rendering tree = component.toJSON(); expect(tree).toMatchSnapshot(); });
When you run npm test
or jest
, this will produce an output file like this:
// __tests__/__snapshots__/Link.react-test.js.snap exports[`Link changes the class when hovered 1`] = ` <a className="normal" href="http://www.facebook.com" onMouseEnter={[Function]} onMouseLeave={[Function]}> Facebook </a> `; exports[`Link changes the class when hovered 2`] = ` <a className="hovered" href="http://www.facebook.com" onMouseEnter={[Function]} onMouseLeave={[Function]}> Facebook </a> `; exports[`Link changes the class when hovered 3`] = ` <a className="normal" href="http://www.facebook.com" onMouseEnter={[Function]} onMouseLeave={[Function]}> Facebook </a> `;
The next time you run the tests, the rendered output will be compared to the previously created snapshot. The snapshot should be committed along code changes. When a snapshot test fails, you need to inspect whether it is an intended or unintended change. If the change is expected you can invoke Jest with jest -u
to overwrite the existing snapshot.
The code for this example is available at examples/snapshot.
DOM Testing #
If you'd like to assert, and manipulate your rendered components you can use Enzyme or React's TestUtils. We use Enzyme for this example.
You have to run npm install --save-dev enzyme react-addons-test-utils
to use Enzyme (react-addons-test-utils
is a peer dependency).
Let's 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}; // bind 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> ); } }
We use Enzyme's shallow renderer 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', () => { // Render 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'); });
The code for this example is available at examples/enzyme.
Custom transformers #
If you need more advanced functionality, you can also build your own transformer. Instead of using babel-jest, here is an example of using babel:
// custom-transformer.js 'use strict'; const babel = require('babel-core'); const jestPreset = require('babel-preset-jest'); module.exports = { process(src, filename) { if (babel.util.canCompile(filename)) { return babel.transform(src, { filename, presets: [jestPreset], retainLines: true, }).code; } return src; }, };
Don't forget to install the babel-core
and babel-preset-jest
packages for this example to work.
To make this work with Jest you need to update your Jest configuration with this: "transform": {"\\.js$": "path/to/custom-transformer.js"}
.
If you'd like to build a transformer with babel support, you can also use babel-jest to compose one and pass in your custom configuration options:
const babelJest = require('babel-jest'); module.exports = babelJest.createTransformer({ presets: ['my-custom-preset'], });