Desenvolvimento React.js orientado a testes: teste de unidade React.js com Enzyme e Jest
Publicados: 2022-03-11Qualquer pedaço de código que não tenha testes é considerado código legado, de acordo com Michael Feathers. Portanto, uma das melhores maneiras de evitar a criação de código legado é usar o desenvolvimento orientado a testes (TDD).
Embora existam muitas ferramentas disponíveis para teste de unidade JavaScript e React.js, neste post, usaremos Jest e Enzyme para criar um componente React.js com funcionalidade básica usando TDD.
Por que usar o TDD para criar um componente React.js?
O TDD traz muitos benefícios ao seu código — uma das vantagens da alta cobertura de teste é que ele permite a refatoração de código fácil, mantendo seu código limpo e funcional.
Se você já criou um componente React.js antes, percebeu que o código pode crescer muito rápido. Ele é preenchido com muitas condições complexas causadas por declarações relacionadas a mudanças de estado e chamadas de serviço.
Todo componente sem testes de unidade possui código legado que se torna difícil de manter. Poderíamos adicionar testes de unidade depois de criarmos o código de produção. No entanto, podemos correr o risco de ignorar alguns cenários que deveriam ter sido testados. Ao criar testes primeiro, temos uma chance maior de cobrir todos os cenários lógicos em nosso componente, o que facilitaria a refatoração e a manutenção.
Como fazemos o teste de unidade de um componente React.js?
Existem muitas estratégias que podemos usar para testar um componente React.js:
- Podemos verificar que uma determinada função em
props
foi chamada quando certo um evento é despachado. - Também podemos obter o resultado da função de
render
dado o estado do componente atual e combiná-lo com um layout predefinido. - Podemos até verificar se o número de filhos do componente corresponde a uma quantidade esperada.
Para usar essas estratégias, vamos usar duas ferramentas que são úteis para trabalhar com testes em React.js: Jest e Enzyme.
Usando Jest para criar testes de unidade
Jest é um framework de teste de código aberto criado pelo Facebook que possui uma ótima integração com o React.js. Inclui uma ferramenta de linha de comando para execução de testes semelhante ao que Jasmine e Mocha oferecem. Ele também nos permite criar funções simuladas com configuração quase zero e fornece um conjunto muito bom de matchers que facilita a leitura das asserções.
Além disso, oferece um recurso muito bom chamado “teste de instantâneo”, que nos ajuda a verificar e verificar o resultado da renderização do componente. Usaremos o teste de instantâneo para capturar a árvore de um componente e salvá-lo em um arquivo que podemos usar para compará-lo com uma árvore de renderização (ou o que passarmos para a função expect
como primeiro argumento).
Usando Enzyme para Montar Componentes React.js
O Enzyme fornece um mecanismo para montar e percorrer as árvores de componentes do React.js. Isso nos ajudará a obter acesso às suas próprias propriedades e estado, bem como a seus props filhos, a fim de executar nossas asserções.
A Enzyme oferece duas funções básicas para montagem de componentes: shallow
e mount
. A função shallow
carrega na memória apenas o componente raiz, enquanto mount
carrega a árvore DOM completa.
Vamos combinar Enzyme e Jest para montar um componente React.js e executar asserções sobre ele.
Configurando nosso ambiente
Você pode dar uma olhada neste repositório, que tem a configuração básica para executar este exemplo.
Estamos usando as seguintes versões:
{ "react": "16.0.0", "enzyme": "^2.9.1", "jest": "^21.2.1", "jest-cli": "^21.2.1", "babel-jest": "^21.2.0" }
Criando o componente React.js usando TDD
O primeiro passo é criar um teste com falha que tentará renderizar um componente React.js usando a função superficial da enzima.
// MyComponent.test.js import React from 'react'; import { shallow } from 'enzyme'; import MyComponent from './MyComponent'; describe("MyComponent", () => { it("should render my component", () => { const wrapper = shallow(<MyComponent />); }); });
Após executar o teste, recebemos o seguinte erro:
ReferenceError: MyComponent is not defined.
Em seguida, criamos o componente fornecendo a sintaxe básica para fazer o teste passar.
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div />; } }
Na próxima etapa, garantiremos que nosso componente renderize um layout de interface do usuário predefinido usando a função toMatchSnapshot
do Jest.
Depois de chamar esse método, o Jest cria automaticamente um arquivo de instantâneo chamado [testFileName].snap
, que é adicionado à pasta __snapshots__
.
Este arquivo representa o layout da interface do usuário que esperamos de nossa renderização de componentes.
No entanto, como estamos tentando fazer TDD puro , devemos criar esse arquivo primeiro e depois chamar a função toMatchSnapshot
para fazer o teste falhar.
Isso pode parecer um pouco confuso, já que não sabemos qual formato o Jest usa para representar esse layout.
Você pode ficar tentado a executar a função toMatchSnapshot
primeiro e ver o resultado no arquivo de instantâneo, e essa é uma opção válida. No entanto, se realmente quisermos usar TDD puro , precisamos aprender como os arquivos de instantâneos são estruturados.
O arquivo de instantâneo contém um layout que corresponde ao nome do teste. Isso significa que, se nosso teste tiver este formato:
desc("ComponentA" () => { it("should do something", () => { … } });
Devemos especificar isso na seção de exportações: Component A should do something 1
.
Você pode ler mais sobre o teste de instantâneo aqui.
Então, primeiro criamos o arquivo MyComponent.test.js.snap
.
//__snapshots__/MyComponent.test.js.snap exports[`MyComponent should render initial layout 1`] = ` Array [ <div> <input type="text" /> </div>, ] `;
Em seguida, criamos o teste de unidade que verificará se o instantâneo corresponde aos elementos filho do componente.

// MyComponent.test.js ... it("should render initial layout", () => { // when const component = shallow(<MyComponent />); // then expect(component.getElements()).toMatchSnapshot(); }); ...
Podemos considerar components.getElements
como resultado do método render.
Passamos esses elementos para o método expect
para executar a verificação no arquivo de instantâneo.
Após executar o teste, recebemos o seguinte erro:
Received value does not match stored snapshot 1. Expected: - Array [ <div> <input type="text” /> </div>, ] Actual: + Array []
Jest está nos dizendo que o resultado de component.getElements
não corresponde ao instantâneo. Então, fazemos esse teste passar adicionando o elemento input em MyComponent
.
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div><input type="text" /></div>; } }
A próxima etapa é adicionar funcionalidade à input
executando uma função quando seu valor for alterado. Fazemos isso especificando uma função na prop onChange
.
Primeiro precisamos alterar o instantâneo para fazer o teste falhar.
//__snapshots__/MyComponent.test.js.snap exports[`MyComponent should render initial layout 1`] = ` Array [ <div> <input onChange={[Function]} type="text" /> </div>, ] `;
Uma desvantagem de modificar o instantâneo primeiro é que a ordem dos adereços (ou atributos) é importante.
O Jest classificará em ordem alfabética os adereços recebidos na função expect
antes de verificá-los em relação ao instantâneo. Portanto, devemos especificá-los nessa ordem.
Após executar o teste, recebemos o seguinte erro:
Received value does not match stored snapshot 1. Expected: - Array [ <div> onChange={[Function]} <input type="text”/> </div>, ] Actual: + Array [ <div> <input type=”text” /> </div>, ]
Para fazer esse teste passar, podemos simplesmente fornecer uma função vazia para onChange
.
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div><input onChange={() => {}} type="text" /></div>; } }
Em seguida, garantimos que o estado do componente mude após o evento onChange
ser despachado.
Para fazer isso, criamos um novo teste de unidade que chamará a função onChange
na entrada passando um evento para imitar um evento real na interface do usuário.
Em seguida, verificamos se o estado do componente contém uma chave chamada input
.
// MyComponent.test.js ... it("should create an entry in component state", () => { // given const component = shallow(<MyComponent />); const form = component.find('input'); // when form.props().onChange({target: { name: 'myName', value: 'myValue' }}); // then expect(component.state('input')).toBeDefined(); });
Agora temos o seguinte erro.
Expected value to be defined, instead received undefined
Isso indica que o componente não possui uma propriedade no estado chamada input
.
Fazemos o teste passar definindo essa entrada no estado do componente.
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div><input onChange={(event) => {this.setState({input: ''})}} type="text" /></div>; } }
Em seguida, precisamos garantir que um valor seja definido na nova entrada de estado. Vamos obter esse valor do evento.
Então, vamos criar um teste que garanta que o estado contenha esse valor.
// MyComponent.test.js ... it("should create an entry in component state with the event value", () => { // given const component = shallow(<MyComponent />); const form = component.find('input'); // when form.props().onChange({target: { name: 'myName', value: 'myValue' }}); // then expect(component.state('input')).toEqual('myValue'); }); ~~~ Not surprisingly, we get the following error. ~~ Expected value to equal: "myValue" Received: ""
Finalmente, fazemos esse teste passar obtendo o valor do evento e definindo-o como o valor de entrada.
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div><input onChange={(event) => { this.setState({input: event.target.value})}} type="text" /></div>; } }
Depois de garantir que todos os testes passem, podemos refatorar nosso código.
Podemos extrair a função passada na prop onChange
para uma nova função chamada updateState
.
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { updateState(event) { this.setState({ input: event.target.value }); } render() { return <div><input onChange={this.updateState.bind(this)} type="text" /></div>; } }
Agora temos um componente React.js simples criado usando TDD.
Resumo
Neste exemplo, tentamos usar TDD puro seguindo cada passo escrevendo o mínimo de código possível para falhar e passar nos testes.
Algumas das etapas podem parecer desnecessárias e podemos ficar tentados a ignorá-las. No entanto, sempre que pularmos qualquer etapa, acabaremos usando uma versão menos pura do TDD.
Usar um processo TDD menos rigoroso também é válido e pode funcionar bem.
Minha recomendação para você é evitar pular etapas e não se sentir mal se achar difícil. TDD é uma técnica que não é fácil de dominar, mas definitivamente vale a pena fazer.
Se você estiver interessado em aprender mais sobre TDD e o desenvolvimento orientado a comportamento (BDD), leia Your Boss Won't Appreciate TDD pelo colega Toptaler Ryan Wilcox.