Testing React Apps
At Facebook, we use Jest to test React applications.
Instalare
Instalare cu Create React App
If you are new to React, we recommend using Create React App. Este gata pentru utilizare şi este livrat cu Jest! You will only need to add react-test-renderer
for rendering snapshots.
Rulare
yarn add --dev react-test-renderer
Instalare fără Create React App
Dacă aveţi o aplicaţie existentă trebuie să instalaţi câteva pachete pentru a face totul să funcționeze bine împreună. Noi folosim pachetul babel-jest
şi presetarea babel react
pentru a transforma codul nostru în mediul de testare. De asemenea, consultaţi utilizarea babel.
Rulare
yarn add --dev jest babel-jest babel-preset-env babel-preset-react react-test-renderer
Fișierul package.json
ar trebui să arate în felul următor (unde <current-version>
este numărul real al celei mai recente versiuni pentru pachetul respectiv). Vă rugăm să adăugaţi script-uri şi configurările pentru jest:
// package.json
"dependencies": {
"react": "<current-version>",
"react-dom": "<current-version>"
},
"devDependencies": {
"babel-jest": "<current-version>",
"babel-preset-env": "<current-version>",
"babel-preset-react": "<current-version>",
"jest": "<current-version>",
"react-test-renderer": "<current-version>"
},
"scripts": {
"test": "jest"
}
// .babelrc
{
"presets": ["env", "react"]
}
Și sunteți gata de drum!
Testarea de imagine
Haideți să creăm un test de imagine pentru o componentă Link care randează hyperlink-uri:
// 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>
);
}
}
Acum haideţi să utilizăm procesorul de randare React şi funcționalitatea din Jest pentru testarea de imagine pentru a interacţiona cu componenta şi a captura rezultatul şi a crea un fişier de imagine:
// Link.react.test.js
import React from 'react';
import renderer from 'react-test-renderer';
import Link from '../Link.react';
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 yarn 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>
`;
Data viitoare când executaţi testele, rezultatul va fi comparat cu imaginea creată anterior. The snapshot should be committed along with code changes. Atunci când un test de imagine eșuează, trebuie să inspectați dacă este vorba de o modificare intenționată sau neintenţionată. În cazul în care schimbarea este intenționată se poate apela Jest cu jest -u
pentru a suprascrie imaginea existentă.
The code for this example is available at examples/snapshot.
Snapshot Testing with Mocks, Enzyme and React 16
There's a caveat around snapshot testing when using Enzyme and React 16+. If you mock out a module using the following style:
jest.mock('../SomeDirectory/SomeComponent', () => 'SomeComponent');
Then 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 meant to render a React component, start its name with an uppercase letter.
React 16 triggers these warnings due to how it checks element types, and the mocked module fails these checks. Your options are:
Render as text. This way you won't see the props passed to the mock component in the snapshot, but it's straightforward: js jest.mock('./SomeComponent', () => () => 'SomeComponent');
Render as a custom element. DOM "custom elements" aren't checked for anything and shouldn't fire warnings. They are lowercase and have a dash in the name. tsx jest.mock('./Widget', () => () => <mock-widget />);
Use
react-test-renderer
. The test renderer doesn't care about element types and will happily accept e.g.SomeComponent
. You could check snapshots using the test renderer, and check component behavior separately using Enzyme.Disable warnings all together (should be done in your jest setup file): js jest.mock('fbjs/lib/warning', () => require('fbjs/lib/emptyFunction')); This shouldn't normally be your option of choice as useful warnings could be lost. However, in some cases, for example when testing react-native's components we are rendering react-native tags into the DOM and many warnings are irrelevant. Another option is to swizzle the console.warn and suppress specific warnings.
Testarea DOM
If you'd like to assert, and manipulate your rendered components you can use react-testing-library, Enzyme, or React's TestUtils. The following two examples use react-testing-library and Enzyme.
react-testing-library
You have to run yarn add --dev @testing-library/react
to use react-testing-library.
Let's implement a 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
// https://reactjs.org/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';
// Note: running cleanup afterEach is done automatically for you in @testing-library/react@9.0.0 or higher
// unmount 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();
});
The code for this example is available at examples/react-testing-library.
Enzyme
You have to run yarn add --dev enzyme
to use Enzyme. If you are using a React version below 15.5.0, you will also need to install react-addons-test-utils
.
Let's rewrite the test from above using Enzyme instead of react-testing-library. În acest exemplu vom folosi un renderer superficial din Enzyme.
// __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.
Transformatoare personalizate
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-core
:
// 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],
});
}
return src;
},
};
Nu uitaţi să instalaţi pachetele babel-core
şi babel-preset-jest
pentru ca acest exemplu să funcționeze.
Pentru a face acest lucru cu Jest trebuie să actualizați configurația Jest cu: "transform": {"\\.js$": "calea/catre/transformatorul-personalizat.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'],
});