Snapshot Testing, facilitando a vida em muitos casos!

Jest: Snapshot Testing com React e Enzyme

Eduardo Rabelo
6 min readDec 8, 2016

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.

Usando 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! 🙏🏼

--

--

Responses (4)