Como os componentes do React facilitam o teste de interface do usuário

Publicados: 2022-03-11

Testar back-ends é fácil. Você escolhe sua linguagem de escolha, emparelha-a com seu framework favorito, escreve alguns testes e clica em “executar”. Seu console diz “Eba! Funciona!" Seu serviço de integração contínua executa seus testes em cada push, a vida é ótima.

Claro, o desenvolvimento orientado a testes (TDD) é estranho no começo, mas um ambiente previsível, vários executores de teste, ferramentas de teste incorporadas às estruturas e suporte à integração contínua facilitam a vida. Cinco anos atrás eu achava que os testes eram a solução para todos os problemas que eu já tive.

Então o Backbone ficou grande.

Todos nós mudamos para o MVC front-end. Nossos back-ends testáveis ​​tornaram-se servidores de banco de dados glorificados. Nosso código mais complicado foi movido para o navegador. E nossos aplicativos não eram mais testáveis ​​na prática.

Isso porque testar o código front-end e os componentes da interface do usuário é meio difícil.

Não é tão ruim se tudo o que queremos é verificar se nossos modelos se comportam bem. Ou que chamar uma função mudará o valor correto. Tudo o que precisamos fazer para o teste de unidade React é:

  • Escreva módulos bem formados e isolados.
  • Use os testes Jasmine ou Mocha (ou qualquer outro) para executar funções.
  • Use um executor de teste, como Karma ou Chutzpah.

É isso. Nosso código é testado por unidade.

Antigamente, executar testes de front-end era a parte mais difícil. Cada estrutura tinha suas próprias ideias e, na maioria dos casos, você acabava com uma janela do navegador que atualizava manualmente toda vez que desejasse executar os testes. Claro, você sempre esqueceria. Pelo menos, eu sei que fiz.

Em 2012, Vojta Jina lançou o Karma runner (chamado Testacular na época). Com o Karma, o teste de front-end se torna um cidadão completo da cadeia de ferramentas. Nossos testes React são executados em um terminal ou em um servidor de integração contínua, eles são executados novamente quando alteramos um arquivo e podemos até testar nosso código em vários navegadores ao mesmo tempo.

O que mais poderíamos desejar? Bem, para realmente testar nosso código front-end.

O teste de front-end requer mais do que apenas testes de unidade

O teste de unidade é ótimo: é a melhor maneira de ver se um algoritmo faz a coisa certa todas as vezes ou de verificar nossa lógica de validação de entrada, transformações de dados ou qualquer outra operação isolada. O teste de unidade é perfeito para os fundamentos.

Mas o código front-end não é sobre manipulação de dados. Trata-se de eventos do usuário e renderização das visualizações certas no momento certo. Front-ends são sobre usuários.

Aqui está o que queremos ser capazes de fazer:

  • Testar eventos do usuário do React
  • Teste a resposta a esses eventos
  • Certifique-se de que as coisas certas sejam renderizadas no momento certo
  • Execute testes em muitos navegadores
  • Reexecutar testes em alterações de arquivo
  • Trabalhe com sistemas de integração contínua como o Travis

Nos dez anos em que tenho feito isso, não encontrei uma maneira decente de testar a interação do usuário e visualizar a renderização até começar a cutucar o React.

Teste de unidade do React: componentes da interface do usuário

React é a maneira mais fácil de atingir esses objetivos. Em parte, por causa de como isso nos força a arquitetar aplicativos usando padrões testáveis, em parte, porque existem fantásticos utilitários de teste React.

Se você nunca usou React antes, você deveria dar uma olhada no meu livro React+d3.js . É voltado para visualizações, mas me disseram que é “uma introdução leve e incrível” ao React.

React nos força a construir tudo como “componentes”. Você pode pensar nos componentes do React como widgets ou como pedaços de HTML com alguma lógica. Eles seguem muitos dos melhores princípios da programação funcional, exceto que são objetos.

Por exemplo, dado o mesmo conjunto de parâmetros, um componente React sempre renderizará a mesma saída. Não importa quantas vezes ele seja renderizado, não importa quem o renderize, não importa onde colocamos a saída. Sempre o mesmo. Como resultado, não precisamos executar andaimes complexos para testar os componentes do React. Eles se preocupam apenas com suas propriedades, sem necessidade de rastreamento de variáveis ​​globais e objetos de configuração.

Conseguimos isso em grande parte evitando o estado. Você chamaria isso de transparência referencial na programação funcional. Eu não acho que haja um nome especial para isso no React, mas os documentos oficiais recomendam evitar o uso de state o máximo possível.

Quando se trata de testar as interações do usuário, o React nos cobre com eventos vinculados a retornos de chamada de função. É fácil configurar espiões de teste e garantir que um evento de clique chame a função correta. E como os componentes do React se renderizam, podemos apenas acionar um evento de clique e verificar o HTML quanto a alterações. Isso funciona porque um componente React se preocupa apenas consigo mesmo. Um clique aqui não muda as coisas . Nunca teremos que lidar com um ninho de manipuladores de eventos, apenas chamadas de funções bem definidas.

Ah, e como o React é mágico, não precisamos nos preocupar com o DOM. O React usa o chamado DOM virtual para renderizar componentes em uma variável JavaScript. E uma referência ao DOM virtual é tudo o que precisamos para testar os componentes do React, na verdade.

É muito doce.

TestUtils do React

O React vem com um conjunto de TestUtils . Existe até um executor de testes recomendado chamado Jest, mas eu não gosto. Vou explicar o porquê daqui a pouco. Primeiro, o TestUtils .

Nós os obtemos fazendo algo como require('react/addons').addons.TestUtils . Este é nosso ponto de entrada para testar as interações do usuário e verificar a saída.

O React TestUtils nos permite renderizar um componente React colocando seu DOM em uma variável, em vez de inseri-lo em uma página. Por exemplo, para renderizar um componente React, faríamos algo assim:

 var component = TestUtils.renderIntoDocument( <MyComponent /> );

Então podemos usar TestUtils para verificar se todos os filhos foram renderizados. Algo assim:

 var h1 = TestUtils.findRenderedDOMComponentWithTag( component, 'h1' );

findRenderedDOMComponentWithTag fará o que parece: percorrer os filhos, encontrar o componente que estamos procurando e devolvê-lo. O valor retornado se comportará como um componente React.

Podemos então usar getDOMNode() para acessar o elemento DOM bruto e testar seus valores. Para verificar se uma tag h1 no componente diz “A title” , escrevemos isto:

 expect(h1.getDOMNode().textContent) .toEqual("A title");

Juntos, o teste completo ficaria assim:

 it("renders an h1", function () { var component = TestUtils.renderIntoDocument( <MyComponent /> ); var h1 = TestUtils.findRenderedDOMComponentWithTag( component, 'h1' ); expect(h1.getDOMNode().textContent) .toEqual("A title"); });

A parte legal é que TestUtils nos permite acionar eventos de usuário também. Para um evento de clique, escreveríamos algo assim:

 var node = component .findRenderedDOMComponentWithTag('button') .getDOMNode(); TestUtils.Simulate.click(node);

Isso simula um clique e aciona quaisquer ouvintes em potencial, que devem ser métodos de componentes que alteram a saída, o estado ou ambos. Esses ouvintes podem chamar uma função em um componente pai, se necessário.

Todos os casos são simples de testar: O estado alterado está em component.state , podemos acessar a saída com funções DOM normais e chamadas de função com espiões.

Por que não Jéstia?

A documentação oficial do React recomenda o uso de https://facebook.github.io/jest/ como um executor de testes e framework de testes do React. Jest é construído em Jasmine e usa a mesma sintaxe. Além de tudo que você recebe do Jasmine, o Jest também zomba de tudo, exceto do componente que estamos testando. Isso é fantástico em teoria, mas acho chato. Qualquer coisa que ainda não implementamos, ou que venha de uma parte diferente da base de código, é apenas undefined . Embora isso seja bom em muitos casos, pode levar a falhas silenciosas de bugs.

Eu tive problemas para testar um evento de clique, por exemplo. Não importa o que eu tentasse, ele simplesmente não chamava seu ouvinte. Então percebi que a função foi ridicularizada pelo Jest e nunca me disse isso.

Mas a pior ofensa de Jest, de longe, costumava ser que não tinha um modo de relógio para testar automaticamente novas mudanças. Poderíamos executá-lo uma vez, obter resultados de testes e pronto. (Gosto de executar meus testes em segundo plano enquanto trabalho. Caso contrário, esqueço de executá-los.) Hoje em dia isso não é mais um problema.

Ah, e o Jest não suporta a execução de testes React em vários navegadores. Este é um problema menor do que costumava ser, mas sinto que é um recurso importante para aquelas raras ocasiões em que um heisenbug só acontece em uma versão específica do Chrome…

Nota do editor: Desde que este artigo foi escrito originalmente, Jest melhorou substancialmente. Você pode ler nosso tutorial mais recente, React Unit Testing Using Enzyme and Jest, e decidir por si mesmo se o teste Jest está à altura da tarefa hoje em dia.

Teste de reação: um exemplo integrado

De qualquer forma, vimos como um bom teste de front-end React deve funcionar em teoria. Vamos colocá-lo em ação com um pequeno exemplo.

Vamos visualizar diferentes formas de gerar números aleatórios usando um componente de gráfico de dispersão feito com React e d3.js. O código e sua demonstração também estão no Github.

Vamos usar o Karma como um executor de testes, o Mocha como um framework de testes e o Webpack como um carregador de módulos.

A configuração

Nossos arquivos fonte irão para um diretório <root>/src , e colocaremos os testes em um diretório <root>/src/__tests__ . A ideia é que possamos colocar vários diretórios dentro do src , um para cada componente principal, e cada um com seus próprios arquivos de teste. Agrupar código-fonte e arquivos de teste como esse facilita a reutilização de componentes do React em diferentes projetos.

Com a estrutura de diretórios em vigor, podemos instalar dependências como esta:

 $ npm install --save-dev react d3 webpack babel-loader karma karma-cli karma-mocha karma-webpack expect

Se alguma coisa falhar na instalação, tente executar novamente essa parte da instalação. O NPM às vezes falha de maneiras que desaparecem em uma nova execução.

Nosso arquivo package.json deve ficar assim quando terminarmos:

 // package.json { "name": "react-testing-example", "description": "A sample project to investigate testing options with ReactJS", "scripts": { "test": "karma start" }, // ... "homepage": "https://github.com/Swizec/react-testing-example", "devDependencies": { "babel-core": "^5.2.17", "babel-loader": "^5.0.0", "d3": "^3.5.5", "expect": "^1.6.0", "jsx-loader": "^0.13.2", "karma": "^0.12.31", "karma-chrome-launcher": "^0.1.10", "karma-cli": "0.0.4", "karma-mocha": "^0.1.10", "karma-sourcemap-loader": "^0.3.4", "karma-webpack": "^1.5.1", "mocha": "^2.2.4", "react": "^0.13.3", "react-hot-loader": "^1.2.7", "react-tools": "^0.13.3", "webpack": "^1.9.4", "webpack-dev-server": "^1.8.2" } }

Após alguma configuração, poderemos executar testes com npm test ou karma start .

Executando um teste

A configuração

Não há muito para a configuração. Temos que garantir que o Webpack saiba como encontrar nosso código e que o Karma saiba como executar os testes.

Colocamos duas linhas de JavaScript em um arquivo ./tests.webpack.js para ajudar o Karma e o Webpack a jogarem juntos:

 // tests.webpack.js var context = require.context('./src', true, /-test\.jsx?$/); context.keys().forEach(context);

Isso diz ao Webpack para considerar qualquer coisa com um sufixo -test como parte do conjunto de testes.

Configurar o Karma dá um pouco mais de trabalho:

 // karma.conf.js var webpack = require('webpack'); module.exports = function (config) { config.set({ browsers: ['Chrome'], singleRun: true, frameworks: ['mocha'], files: [ 'tests.webpack.js' ], preprocessors: { 'tests.webpack.js': ['webpack'] }, reporters: ['dots'], webpack: { module: { loaders: [ {test: /\.jsx?$/, exclude: /node_modules/, loader: 'babel-loader'} ] }, watch: true }, webpackServer: { noInfo: true } }); };

A maioria dessas linhas são de uma configuração padrão do Karma. Usamos browsers para dizer que os testes devem ser executados no Chrome, frameworks para especificar qual framework de teste estamos usando e singleRun para fazer os testes serem executados apenas uma vez por padrão. Você pode manter o karma rodando em segundo plano com karma start --no-single-run .

Esses três são óbvios. O material do Webpack é mais interessante.

Como o Webpack lida com a árvore de dependências do nosso código, não precisamos especificar todos os nossos arquivos na matriz de files . Precisamos apenas de tests.webpack.js , que requer todos os arquivos necessários.

Usamos a configuração do webpack para dizer ao Webpack o que fazer. Em um ambiente normal, essa parte iria em um arquivo webpack.config.js .

Também dizemos ao Webpack para usar o babel-loader para nossos JavaScripts. Isso nos dá todos os novos recursos sofisticados do ECMAScript2015 e do JSX do React.

Com a configuração do webpackServer , informamos ao Webpack para não imprimir nenhuma informação de depuração. Isso só estragaria nossa saída de teste.

Um componente React e um teste

Com um conjunto de testes em execução, o resto é simples. Temos que fazer um componente que aceite um array de coordenadas aleatórias e crie um elemento <svg> com vários pontos.

Seguindo as melhores práticas de teste do React—ou seja, a prática padrão de TDD—nós escreveremos o teste primeiro, depois o componente React real. Vamos começar com um arquivo de testes vanilla em src/__tests__/ :

 // ScatterPlot-test.jsx var React = require('react/addons'), TestUtils = React.addons.TestUtils, expect = require('expect'), ScatterPlot = require('../ScatterPlot.jsx'); var d3 = require('d3'); describe('ScatterPlot', function () { var normal = d3.random.normal(1, 1), mockData = d3.range(5).map(function () { return {x: normal(), y: normal()}; }); });

Primeiro precisamos do React, seus TestUtils, d3.js, a biblioteca expect e o código que estamos testando. Em seguida, criamos um novo conjunto de testes com describe e criamos alguns dados aleatórios.

Para nosso primeiro teste, vamos garantir que o ScatterPlot renderize um título. Nosso teste vai dentro do bloco describe :

 // ScatterPlot-test.jsx it("renders an h1", function () { var scatterplot = TestUtils.renderIntoDocument( <ScatterPlot /> ); var h1 = TestUtils.findRenderedDOMComponentWithTag( scatterplot, 'h1' ); expect(h1.getDOMNode().textContent).toEqual("This is a random scatterplot"); });

A maioria dos testes seguirá o mesmo padrão:

  1. Render.
  2. Encontre o nó específico.
  3. Verifique o conteúdo.

Como vimos anteriormente, renderIntoDocument renderiza nosso componente, findRenderedDOMComponentWithTag encontra a parte específica que estamos testando e getDOMNode nos dá acesso ao DOM bruto.

A princípio, nosso teste falhará. Para fazer isso passar, temos que escrever o componente que renderiza uma title tag:

 var React = require('react/addons'); var d3 = require('d3'); var ScatterPlot = React.createClass({ render: function () { return ( <div> <h1>This is a random scatterplot</h1> </div> ); } }); module.exports = ScatterPlot;

É isso. O componente ScatterPlot renderiza um <div> com uma tag <h1> contendo o texto esperado, e nosso teste será aprovado. Sim, é mais do que apenas HTML, mas tenha paciência comigo.

Desenhe o resto da coruja

Você pode ver o restante do nosso exemplo no GitHub, conforme mencionado acima. Vamos pular a descrição passo a passo neste artigo, mas o processo geral é o mesmo acima. Eu quero mostrar-lhe um teste mais interessante, no entanto. Um teste que garante que todos os pontos de dados apareçam no gráfico:

 // ScatterPlot-test.jsx it("renders a circle for each datapoint", function () { var scatterplot = TestUtils.renderIntoDocument( <ScatterPlot data={mockData} /> ); var circles = TestUtils.scryRenderedDOMComponentsWithTag( scatterplot, 'circle' ); expect(circles.length).toEqual(5); });

O mesmo de antes. Renderize, encontre nós, verifique o resultado. A parte interessante aqui é desenhar esses nós DOM. Adicionamos alguma mágica do ScatterPlot ao componente ScatterPlot, assim:

 // ScatterPlot.jsx componentWillMount: function () { this.yScale = d3.scale.linear(); this.xScale = d3.scale.linear(); this.update_d3(this.props); }, componentWillReceiveProps: function (newProps) { this.update_d3(newProps); }, update_d3: function (props) { this.yScale .domain([d3.min(props.data, function (d) { return dy; }), d3.max(props.data, function (d) { return dy; })]) .range([props.point_r, Number(props.height-props.point_r)]); this.xScale .domain([d3.min(props.data, function (d) { return dx; }), d3.max(props.data, function (d) { return dx; })]) .range([props.point_r, Number(props.width-props.point_r)]); }, ...

Usamos componentWillMount para configurar escalas d3 vazias para os domínios X e Y e componentWillReceiveProps para garantir que sejam atualizados quando algo mudar. Em seguida, update_d3 certifica-se de definir o domain e o range para ambas as escalas.

Usaremos as duas escalas para traduzir entre valores aleatórios em nosso conjunto de dados e posições na imagem. A maioria dos geradores aleatórios retorna números no intervalo [0,1] , que é muito pequeno para ser visto como pixels.

Em seguida, adicionamos os pontos ao método de renderização do nosso componente:

 // ScatterPlot.jsx render: function () { return ( <div> <h1>This is a random scatterplot</h1> <svg width={this.props.width} height={this.props.height}> {this.props.data.map(function (pos, i) { var key = "circle-"+i; return ( <circle key={key} cx={this.xScale(pos.x)} cy={this.yScale(pos.y)} r={this.props.point_r} /> ); }.bind(this))}; </svg> </div> ); }

Esse código passa pelo array this.props.data e adiciona um elemento <circle> para cada ponto de dados. Simples.

Não se desespere mais por seus testes de interface do usuário, com o teste de componentes React.
Tweet

Se você quiser saber mais sobre como combinar React e d3.js para criar componentes de visualização de dados, esse é outro ótimo motivo para conferir meu livro, React+d3.js .

Teste automatizado de componentes React: mais fácil do que parece

Isso é tudo o que precisamos saber sobre como escrever componentes front-end testáveis ​​com React. Para ver mais código testando componentes do React, confira a base de código de exemplo de teste do React no Github, conforme mencionado acima.

Aprendemos que:

  1. React nos força a modularizar e encapsular.
  2. Isso torna o teste de interface do usuário do React fácil de automatizar.
  3. Os testes de unidade não são suficientes para front-ends.
  4. Karma é um grande corredor de teste.
  5. Jest tem potencial, mas ainda não chegou lá. (Ou talvez agora seja.)

Se você gostou deste artigo, siga-me no Twitter e deixe um comentário abaixo. Obrigado por ler e feliz teste React!

Relacionado: Como otimizar componentes para melhorar o desempenho do React