Testear Aplicaciones React
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
- npm
- Yarn
- pnpm
npm install --save-dev react-test-renderer
yarn add --dev react-test-renderer
pnpm add --save-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
- npm
- Yarn
- pnpm
npm install --save-dev jest babel-jest @babel/preset-env @babel/preset-react react-test-renderer
yarn add --dev jest babel-jest @babel/preset-env @babel/preset-react react-test-renderer
pnpm add --save-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:
{
"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"
}
}
module.exports = {
presets: [
'@babel/preset-env',
['@babel/preset-react', {runtime: 'automatic'}],
],
};
Y así, ¡Estás listo para despegar!
Test con Instantánea
Vamos a crear un snapshot test para un componente Link que renderé un hyperlink:
import {useState} from 'react';
const STATUS = {
HOVERED: 'hovered',
NORMAL: 'normal',
};
export default function Link({page, children}) {
const [status, setStatus] = useState(STATUS.NORMAL);
const onMouseEnter = () => {
setStatus(STATUS.HOVERED);
};
const onMouseLeave = () => {
setStatus(STATUS.NORMAL);
};
return (
<a
className={status}
href={page || '#'}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
{children}
</a>
);
}
Examples are using Function components, but Class components can be tested in the same way. See React: Function and Class Components. Reminders that with Class components, we expect Jest to be used to test props and not methods directly.
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:
import renderer from 'react-test-renderer';
import Link from '../Link';
it('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
renderer.act(() => {
tree.props.onMouseEnter();
});
// re-rendering
tree = component.toJSON();
expect(tree).toMatchSnapshot();
// manually trigger the callback
renderer.act(() => {
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:
exports[`changes the class when hovered 1`] = `
<a
className="normal"
href="http://www.facebook.com"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
Facebook
</a>
`;
exports[`changes the class when hovered 2`] = `
<a
className="hovered"
href="http://www.facebook.com"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
Facebook
</a>
`;
exports[`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.
The code for this example is available at examples/snapshot.
Snapshot Testing with Mocks, Enzyme and 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');
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):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.
jest.mock('fbjs/lib/warning', () => require('fbjs/lib/emptyFunction'));
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
- npm
- Yarn
- pnpm
npm install --save-dev @testing-library/react
yarn add --dev @testing-library/react
pnpm add --save-dev @testing-library/react
Let's implement a checkbox which swaps between two labels:
import {useState} from 'react';
export default function CheckboxWithLabel({labelOn, labelOff}) {
const [isChecked, setIsChecked] = useState(false);
const onChange = () => {
setIsChecked(!isChecked);
};
return (
<label>
<input type="checkbox" checked={isChecked} onChange={onChange} />
{isChecked ? labelOn : labelOff}
</label>
);
}
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
- npm
- Yarn
- pnpm
npm install --save-dev enzyme
yarn add --dev enzyme
pnpm add --save-dev 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.
import Enzyme, {shallow} from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import CheckboxWithLabel from '../CheckboxWithLabel';
Enzyme.configure({adapter: new Adapter()});
it('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()).toBe('Off');
checkbox.find('input').simulate('change');
expect(checkbox.text()).toBe('On');
});
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
:
'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.