Desenvolvimento React.js orientado a testes: teste de unidade React.js com Enzyme e Jest

Publicados: 2022-03-11

Qualquer 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.

Etapas de TDD para criar um componente de reação

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.