Testing React Apps
No Facebook, nós usamos Jest para testar aplicações React.
Instalação #
Instalação com Create React App #
Se você está apenas começando com React, recomendamos o uso de Create React App. Está pronto para uso e vem com Jest! Você não precisa fazer quaisquer etapas adicionais para instalação e pode ir direto para a próxima seção.
Instalação sem Create React App #
Se você tiver uma aplicação existente vai precisar instalar alguns pacotes para que tudo funcione bem junto. Estamos usando o pacote babel-jest
e o preset react
do Babel para transformar nosso código dentro do ambiente de teste. Consulte também usando Babel.
Execute
npm install --save-dev jest babel-jest babel-preset-es2015 babel-preset-react react-test-renderer
Seu package.json
deve parecer algo como isto (onde <current-version>
é o número da versão mais recente para o pacote). Por favor, adicione as entradas scripts e de configuração jest:
// 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"] }
E você está pronto para ir!
Teste de Snapshot #
Vamos criar um teste de snapshot para um componente Link que renderiza hiperlinks:
// 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> ); } }
Agora vamos usar o renderer de teste do React e o recurso de snapshot do Jest para interagir com o componente e capturar a saída renderizada e criar um arquivo de snapshot:
// 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(); });
Quando você executa o npm test
ou jest
, isto irá produzir um arquivo de saída como este:
// __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> `;
Da próxima vez que você executar os testes, a saída renderizada será comparada ao snapshot criado anteriormente. O snapshot deve ser comitado (committed, em inglês) junto com as alterações de código. Quando um teste de snapshot falhar, você precisa inspecionar se é uma mudança pretendida ou não intencional. Se a mudança é esperada, você pode invocar Jest com jest -u
para substituir o snapshot existente.
O código para este exemplo está disponível em examples/snapshot.
Testando o DOM #
Se você quiser afirmar (assert, em inglês) e manipular seus componentes renderizados você pode usar Enzyme ou TestUtils do React. Nós usamos Enzyme para esse exemplo.
Você tem que executar npm install --save-dev enzyme
para usar Enzyme. Se você estiver usando um React abaixo da versão 15.5.0, você também precisará instalar react-addons-test-utils
.
Vamos implementar um checkbox simples que alterna entre dois 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> ); } }
Nós usamos o renderizador superficial do Enzyme neste exemplo.
// __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'); });
O código para esse exemplo está disponível em examples/enzyme.
Transformadores personalizados #
Se você precisar de funcionalidades mais avançadas, você também pode construir seu próprio transformador. Em vez de usar babel-jest, aqui está um exemplo do uso de 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; }, };
Não se esqueça de instalar os pacotes babel-core
e babel-preset-jest
para que esse exemplo funcione.
Para fazer isto funcionar com Jest você precisa atualizar sua configuração Jest com isso: "transform": {"\\.js$": "path/to/custom-transformer.js"}
.
Se você gostaria de construir um transformador com suporte para o babel, você também pode usar babel-jest para compor um e passe em suas opções de configuração personalizada:
const babelJest = require('babel-jest'); module.exports = babelJest.createTransformer({ presets: ['my-custom-preset'], });