Jest: Snapshot Testing com React e Enzyme
Algumas pessoas dizem que testar componentes React é desnecessário em muitos casos, mas, existem outros casos que vale a pena testa-los:
- biblioteca de componentes
- projetos open source
- integração com bibliotecas de terceiros
- bugs, para previnir regressões na funcionalidade
Eu já tentei várias combinações de ferramentas, até finalmente achar uma combinação que me dá gosto recomendar para outros desenvolvedores:
- Jest, como test runner
- Enzyme, biblioteca de utilidades para testar componentes React
- enzyme-to-json, converte as funções do Enzyme para funcionar com os testes de snapshot do Jests.
Para a maioria dos meus testes, eu utilizo shallow rendering do Enzyme com o snapshot testing do Jest.
Shallow Rendering
O shallow rendering renderiza apenas o componente, sem seus componentes filhos. Então, caso você mude algo em um componente filho, isso não irá modificar a renderização rasa (traduzindo para o português) do seu componente. Ou um bug, que seja introduzido em algum componente filho, não irá quebrar seus testes. E também, não necessita de nenhum pedaço do DOM.
Por exemplo, um componente como:
const ButtonWithIcon = ({icon, children}) => (
<button><Icon icon={icon} />{children}</button>
)
Será renderizado pelo React como:
<button>
<i class="icon icon_coffee"></i>
Hello Jest!
</button>
Usando a renderização rasa, o resultado final é:
<button>
<Icon icon="coffee" />
Hello Jest!
</button>
Percebeu como o componente não foi renderizado? :)
Snapshot Testing
Jest traz uma nova maneira de testar componentes React, o snapshot testing é uma maneira de armazenar o resultado do seu componente em pleno texto.
Parece meio doido, certo? A lógica é simples, você irá dizer ao Jest que o resultado da renderização desse componente sempre será o mesmo (baseado nos mesmos parâmetros) e que ele nunca deve mudar, acidentalmente ou não. Jest irá salvar isso em um arquivo que se parece com:
exports[`test should render a label 1`] = `
<label
className="isBlock">
Hello Jest!
</label>
`exports[`test should render a small label 1`] = `
<label
className="isBlock isSmall">
Hello Jest!
</label>
`
Toda vez que você mudar a marcação do seu componente, Jest irá mostrar a diferença entre o snapshot e o atual resultado final e irá perguntar se as mudanças são intencionais.
Caso sim (ou seja, você realmente alterou o componente), Jest irá armazenar os snapshosts em uma pasta como: __snapshosts__/Componente.spec.js.snap.
Você terá cobertura completa do resultado final de todos os estado do seu componente, podendo comparar o anterior e o atual, verificando o que deu errado ou certo nas suas alterações.
Uma regra valiosa: Você precisa comitar esses arquivos junto do seu código.
Porquê Jest?
No meu post anterior sobre Jest, nós vimos os benefícios de usá-lo, mas aqui vai alguns pontos:
- É muito rápido ao rodar os testes
- Snapshot Testing, faz a diferença
- Um modo interativo e divertido de rodar os testes, o modo — watch pode retornar apenas os testes relevantes que foram alterados
- Mensagens de erros realmente úteis
- Configuração mais do que simples
- mocks e spies por padrão
- Cobertura de testes com um simples parâmetro na linha de comando
- Um desenvolvimento contínuo e ativo (e transparente, tudo no Github)
Porquê Enzyme?
- Funções úteis para se trabalhar com componentes React, tais como: shallow rendering, renderização de marcação estática, renderização completa do DOM
- Uma API parecida com a do jQuery para buscar elementos, propriedades, atributos, etc.
Mão na massa
Eu normalmente uso Babel e CSS Modules, então os exemplos a seguir irão usar essas configurações, porém, elas são totalmente opcionais.
Primeiramente, vamos instalar o necessário:
yard add --dev jest babel-jest react-addons-test-utils enzyme enzyme-to-json
Teremos um package.json como:
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
},
"jest": {
"setupFiles": ["./test/jest-setup.js"],
"snapshotSerializers": [
"<rootDir>/node_modules/enzyme-to-json/serializer"
]
}
Vamos criar o arquivos jest-setup.js e configurar o ambiente aonde o Jest irá rodar:
// Nada de ficar importando 'enzyme' em todo lugar
import { shallow, render, mount } from 'enzyme';
global.shallow = shallow;
global.render = render;
global.mount = mount;// Pula as mensagens de aviso do 'createElement'
// Mas retorna um erro para qualquer outra
console.error = message => {
if (!/(React.createElement: type should not be null)/.test(message)) {
throw new Error(message);
}
};
Para utilizar CSS Modules no Jest, precisamos adicionar um módulo de proxy para resolver as dependências:
yarn add --dev identity-obj-proxy
E também precisamos alterar a configuração do Jest no package.json:
"jest": {
// ... restante da configuração
"moduleNameMapper": {
"^.+\\.(css|scss)$": "identity-obj-proxy"
}
}
Muito importante: Caso você NÃO esteja usando uma versão do Node.js igual ou maior que 6.x, você irá precisar da flag — harmony (Node.js 4.x ou 5.x), você pode ler mais sobre isso no repositório desse projeto.
Escrevendo testes
Testando a renderização de um componente
Isso é suficiente para a maioria dos dumb components:
it('should render a label', () => {
const wrapper = shallow(
<Label>Hello Jest!</Label>
);
expect(wrapper).toMatchSnapshot();
});it('should render a small label', () => {
const wrapper = shallow(
<Label small>Hello Jest!</Label>
);
expect(wrapper).toMatchSnapshot();
});it('should render a grayish label', () => {
const wrapper = shallow(
<Label light>Hello Jest!</Label>
);
expect(wrapper).toMatchSnapshot();
});
Testando suas "props"
Algumas vezes, você quer ser mais explícito e ver valores reais no seu teste. Nesse caso, você pode usar a API do Enzyme com asserções do Jest:
it('should render a document title', () => {
const wrapper = shallow(
<DocumentTitle title="Events" />
);
expect(wrapper.prop('title')).toEqual('Events');
});it('should render a document title and a parent title', () => {
const wrapper = shallow(
<DocumentTitle title="Events" parent="Event Radar" />
);
expect(wrapper.prop('title')).toEqual('Events — Event Radar');
});
E em alguns casos, você não consegue usar os snapshot testing. Por exemplo, caso você tenha um ID dinâmico ou algo como:
it('should render a popover with a random ID', () => {
const wrapper = shallow(
<Popover>Hello Jest!</Popover>
);
expect(wrapper.prop('id')).toMatch(/Popover\d+/);
});
Testando eventos
Você pode simular os eventos do DOM, tais como click ou change e comparar com o seu snapshot:
it('should render Markdown in preview mode', () => {
const wrapper = shallow(
<MarkdownEditor value="*Hello* Jest!" />
);expect(wrapper).toMatchSnapshot();wrapper.find('[name="toggle-preview"]').simulate('click');expect(wrapper).toMatchSnapshot();
});
E, em alguns casos, você quer ter a renderização completa do DOM para interagir com os componentes filhos e testar os efeitos dos eventos no seu componente:
it('should open a code editor', () => {
const wrapper = mount(
<Playground code={code} />
);expect(wrapper.find('.ReactCodeMirror')).toHaveLength(0);wrapper.find('button').simulate('click');expect(wrapper.find('.ReactCodeMirror')).toHaveLength(1);
});
Testando as funções de eventos
Parecido com a seção anterior, mas ao invés de simular o evento, você quer testar se a função passada para seu handler, realmente foi executado. Você pode usar o snapshot para isso:
it('should pass a selected value to the onChange handler', () => {
const value = '2';
const onChange = jest.fn();
const wrapper = shallow(
<Select items={ITEMS} onChange={onChange} />
);expect(wrapper).toMatchSnapshot();wrapper.find('select').simulate('change', {
target: { value },
});expect(onChange).toBeCalledWith(value);
});
Não é apenas para JSX!
Snapshost Testing não funciona apenas com JSX, na verdade, ele funciona com qualquer função que retorna um JSON, sendo assim, podendo usar a mesma API que você testa seus componentes:
it('should accept custom properties', () => {
const wrapper = shallow(
<Layout
flexBasis={0}
flexGrow={1}
flexShrink={1}
flexWrap="wrap"
justifyContent="flex-end"
alignContent="center"
alignItems="center"
/>
);
expect(wrapper.prop('style')).toMatchSnapshot();
});
Debugando e resolvendo problemas
Você pode usar o modo de debug do Enzyme para ver o resultado da renderização:
const wrapper = shallow(/*~*/);
console.log(wrapper.debug());
Caso seu teste falhe, com o parâmetro — coverage, o diff fica mais ou menos assim:
-<Button
+<Component
Para manter um stack trace bom para debug, tente mudar componentes que usam arrow function para declarações normais de função:
- export default const Button = ({ children }) => {
+ export default function Button({ children }) {
Obrigado por ter lido até aqui, se você gostou do post, manda um 💚 e compartilha no Twitter! Valeu! 🙏🏼
Créditos
- Testing React components with Jest and Enzyme, escrito originalmente por Artem Sapegin