Testing React Apps
At Facebook, we use Jest to test React applications.
Setup
Setup con Create React App
Si eres nuevo en React, te recomendamos usar Create React App. Viene ya listo para usar e incluye Jest! Solo necesitarás añadir react-test-renderer
para renderizar instantáneas.
Ejecutar
yarn add --dev react-test-renderer
Setup sin Create React App
Si tiene alguna aplicación existente, necesitará instalar algunos paquetes para hacer que todo trabaje bien junto. Estamos usando el paquete babel-jest
y react
babel preset para transformar nuestro código dentro de el entorno de prueba. Recomendamos también vea using babel.
Ejecutar
yarn add --dev jest babel-jest @babel/preset-env @babel/preset-react react-test-renderer
Tu archivo package.json
se debe ver como algo como ésto (donde <current-version>
es la ultima versión del paquete). Por favor agregue los scripts y la configuración de jest:
// 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'],
};
Y así, ¡Estás listo para despegar!
Snapshot Testing
Vamos a crear un snapshot test para un componente Link que renderé un hyperlink:
// 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>
);
}
}
Ahora vamos usar las características render y snapshot de React y Jest para interactuar con los componentes y capturar lo que renderea, creando un archivo snapshot:
// 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>
`;
La próxima vez que ejecutes la prueba, lo que se renderee será comparado con el snapshot previamente creado. Se debería aplicar el snapshot junto con los cambios de código. Cuando una prueba con snapshot falla, necesitará inspeccionar si se da por un cambio intencional o no intencional. Si el cambio es intencional, puede invocar Jest con jest -u
para sobre escribir en snapshot existente.
El código de este ejemplo se encuentra disponible en examples/snapshot.
Pruebas de Snapshot con Simulaciones, Enzyme y React 16
Hay una advertencia en torno a las pruebas de snapshot cuando se usa Enzyme y React 16+. Si simulas un módulo usando el estilo siguiente:
jest.mock('../SomeDirectory/SomeComponent', () => 'SomeComponent');
Entonces podrá ver advertencias en la consola:
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 dispara estos avisos debido a cómo comprueba los tipos de elementos y el módulo simulado falla esas comprobaciones. Tus opciones son:
Renderizar como texto. De esta manera no verás los props enviados al componente simulado en el snapshot, pero es sencillo: 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.
DOM Testing
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. 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.
Transformers personalizados
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 {transform} = require('@babel/core');
const jestPreset = require('babel-preset-jest');
module.exports = {
process(src, filename) {
const result = transform(src, {
filename,
presets: [jestPreset],
});
return result || src;
},
};
Don't forget to install the @babel/core
and babel-preset-jest
packages for this example to work.
Para que esto funcione con Jest necesita actualizar la configuración de Jest con ésto: "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'],
});
See dedicated docs for more details.