Refatorando código legado em projetos React — Parte II
Depois do primeiro contato com o projeto, chega o momento de refatorar o código legado. A refatoração tornou-se uma habilidade essencial para os desenvolvedores, é a base por trás da arquitetura moderna e evolucionária do desenvolvimento ágil de software.
Neste artigo é introduzido a definição de refatoração, teste de unidade e teste de regressão, na prática é feito novos testes e a implementação de uma nova funcionalidade em um componente React.
Refatoração
Segundo Martin Fowler em seu livro Refactoring, a refatoração é o processo de alterar um sistema de software de uma maneira que não altere o comportamento externo do código, mas melhora sua estrutura interna. É uma maneira disciplinada de limpar o código minimizando as chances de introduzir erros. Em essência, quando você refatora, você está melhorando o design do código depois que ele foi escrito.
A refatoração é uma prática do dia a dia, é um processo de melhoria contínua do código, então ela não deve ser feita de forma isolada e nem deve ser o objetivo fim do desenvolvimento.
When you have to add a feature to a program but the code is not structured in a convenient way, first refactor the program to make it easy to add the feature, then add the feature.
Refactoring — Martin Fowler
O primeiro passo da refatoração é ter uma sólida suíte de testes. Na Parte I deste artigo foi criado alguns testes de integração para garantir o principal fluxo da aplicação. Porém, apesar do teste de integração trazer vários benefícios, ele possui alguns pontos negativos:
- É lento. Como o código tem que estar integrado, os testes podem levar minutos ou até mesmo horas para serem executados, então isso impossibilita a execução contínua durante o desenvolvimento.
- É frágil. Quanto maior a complexidade do projeto, maior é o desafio de manter todas as integrações em harmonia durante os testes.
- Não consegue simular todos os erros. A aplicação precisa ser resiliente a erros, ou seja, se ocorrer um erro na integração ou em alguma parte interna, a aplicação tem que saber tratar isso para continuar funcionando, principalmente se o erro não afetar os fluxos principais do sistema. Esses erros se tornam inviáveis de ser identificados pelo teste de integração já que eles não ficam visíveis na interface do usuário.
- Não mostra onde o problema está. Como o teste de integração engloba grande parte da complexidade do projeto, muitas vezes será difícil identificar de forma fácil e rápida em que parte está o problema para ser corrigido.
Os motivos citados acima não invalida o uso de teste de integração, pelo contrário, ele é muito importante para garantir que a aplicação está funcionando em sua plenitude, mas reforça a necessidade de outro tipo de teste que contrapõe esses pontos, que é o teste de unidade.
Teste de Unidade
O teste de unidade (unit testing) é um nível de teste de software em que unidades de um software são testadas individualmente.
O termo original em inglês ganhou muita força quando Kent Beck lançou o livro Extreme Programming Explained em 1999, o teste de unidade é um dos elementos da metodologia Extreme Programming (XP) . O termo unit foi utilizado junto ao termo xUnit, que é o nome coletivo de vários frameworks de testes que derivam sua estrutura e funcionalidade do sUnit para Smalltalk, desenvolvido pelo próprio Kent Beck, que posteriormente foi portado para Java com o jUnit por Erich Gamma e Kent Beck. O teste de unidade também é conhecido por fazer parte do famoso Test-Driven Development (TDD) que também foi difundido por você sabe quem? Kent Beck!
A unidade é uma porção do código, pode ser uma função, método, uma classe, um componente, um módulo, depende da linguagem e de sua organização. Por isso a tradução “unidade” é mais significativa do que “unitário” como muitos falam, já vi o termo “teste unitário” até em livros, mas a Associação Brasileira de Letras (ABL) confirmou o termo em resposta ao Maurício Aniche, conforme ele descreve em um post.
Ao escrever os testes, algumas boas práticas devem ser consideradas:
- O código não pode ter nenhum tipo de integração, a unica coisa a ser testada é o próprio código.
- Os testes devem ser independentes, podem ser executados individualmente e em qualquer ordem.
- Devem ser rápidos. O ideal é que cada teste seja executado em poucos milissegundos.
- Cada teste deve fazer somente uma afirmação, a mensagem do teste e da afirmação devem ser claras. Use entradas e dados que façam sentido para a regra de negócio.
Não se preocupe se toda essa teoria é nova para você, a prática irá lhe ajudar a absorver o conteúdo. Então mãos ao código!
O projeto
Caso você não tenha acompanhado o desenvolvimento feito na Parte I, você pode baixar o código pronto através da versão v0.1.1 disponível no endereço:
https://github.com/megatroom/refactoring-react-legacy-code/releases/tag/v0.1.1
Ou utilizando o git pelo comando:
git clone git@github.com:megatroom/refactoring-react-legacy-code.git
cd refactoring-react-legacy-code
git checkout v0.1.1
Testando componente
No projeto surgiu a necessidade de tratar um novo status na lista de cursos que será retornada pela API. O novo status terá o valor "CANCELED"
no campo status
, como no exemplo:
Na tela onde aparece a listagem, o curso terá o texto “Cancelado” no rótulo abaixo do título e terá apenas um botão com o texto “REATIVAR”, conforme demostrado na imagem abaixo:
O React divide a interface de usuário (user interface - UI) em componentes, que são partes independentes, reutilizáveis e é possível testar de forma isolada, então ele atende o requisito de unidade.
As alterações demostradas na Figura 1 afetam apenas o componente CourseCard.js (clique no link para ver o conteúdo do arquivo), que é a interface de cada curso na listagem. Este componente possui alguns pontos que podem ser melhorados com a refatoração, mas antes de refatorar ou implementar a melhoria, o primeiro passo é sempre criar testes para a unidade, então começaremos instalando a biblioteca React Testing Library dentro do diretório /client
:
npm install --save-dev react-testing-library
Para testar todos os diferentes comportamentos do componente é necessário todas as combinações possíveis de curso. Para isso, será criado o arquivo courses.json
dentro do diretório /client/mocks
(o diretório mocks
não existe, você terá que criá-lo) com o seguinte conteúdo:
São cinco cursos com cenários diferentes, sendo que o quinto curso é o cenário novo onde está cancelado. Desta maneira é possível isolar o modelo e reutilizá-lo em diferentes arquivos de testes.
Para o teste, crie o arquivo CourseCard.test.js
dentro do diretório /client/src/components
e nele comece pelas importações:
O React
é necessário pois será utilizado JSX no teste, o courses
é o JSON criado com os dados dos diferentes cenários dos cursos e o CourseCard
é o componente que será testado. Os demais são explicados adiante.
Como cada teste irá montar o componente CourseCard
, é necessário desmontá-lo após cada teste:
E então o primeiro teste:
O render()
monta o componente CourseCard
no DOM semelhante ao jeito que o ReactDOM.render()
faz, só que ele retorna um objeto com vários métodos para nos auxiliar no teste, sendo um deles o método getByText()
que foi utilizado para obter o elemento do DOM através de seu texto.
O principal objetivo aqui é testar o componente como um usuário. Isso significa não depender dos detalhes de implementação, ou até mesmo acessar o seu estado.
Um bom termômetro para saber o que deve ser testado é identificar qual modificação na unidade testada deve fazer o teste falhar. No caso deste teste, se o título mudar da tagh2
para h3
não é importante o bastante, mas o teste tem que falhar caso o título não apareça.
Neste teste foi utilizado o primeiro curso (courses[0]
) para definir as propriedades do componente.
Com isso já é suficiente para executar o primeiro teste. No terminal, no diretório /client
, execute o comando:
npm test
O resultado no terminal será semelhante à imagem abaixo:
A configuração do teste já estava pronta porque o código foi gerado através do Create React App (CRA) que vem por padrão com o Jest. O teste fica em execução esperando qualquer alteração no projeto, e ele consegue reconhecer o que foi modificado e executa novamente os testes referente a modificação, assim como executa novos testes. Esta facilidade aumenta muito a produtividade, você pode deixá-lo em execução para escrever os próximos testes:
No teste do curso pago, o componente CourseCard
passa a usar o componente Link
do React Router que exige o uso de algum provider de rota declarado em algum lugar acima da sua estrutura, então foi necessário usar o provider Router
antes de chamar o componente CourseCard
. Em projetos que utilizam vários providers é uma boa prática criar um componente para encapsular todos eles, assim qualquer mudança não será necessário alterar todos os testes.
Refatorando componente
Depois de analisar o arquivo CourseCard.js em busca de melhorias, a alteração mais simples e clara a ser feita é trocar o nome do componente que está MediaCard
para CourseCard
, assim o nome do componente fica do mesmo nome do arquivo.
Lembre-se de manter o teste de unidade em execução, a cada mudança ele vai ser executado e você poderá ver se quebrou alguma coisa.
No elemento CardMedia
está com o atributo title="Contemplative Reptile"
que provavelmente foi sobra de algum copia-e-cola, o certo seria ter o nome do curso, então altera-se para title={name}
.
Depois dos pequenos ajustes, seguimos para modificações maiores. A variável renderActions
, que é usada para controlar os botões que serão renderizados, acabou gerando repetição de código, pois o que muda é o comportamento, mas a apresentação dos botões são sempre a mesma, então a próxima refatoração é para separar a lógica da apresentação dos botões. As modificações estão comentadas no código abaixo:
Para terminar de separar o comportamento da apresentação falta remover os condicionais status !== ACTIVE
que estão nas propriedades dos elementos CardActionArea
e CardMedia
, então transferimos a regra para uma variável:
const isDisabled = status !== "ACTIVE";
Depois substituímos onde estava a regra pela variável:
<CardActionArea disabled={isDisabled}> // aqui
<CardMedia
className={classNames(classes.media, {
[classes.mediaDisabled]: isDisabled // e aqui
})}
Com essa separação, é possível extrair todo o comportamento para uma função separada e deixar o componente apenas com a apresentação:
Esta técnica de extrair um código para outra função ou método se chama Extract Method. Há muito a ser melhorado ainda, mas as melhorias devem ser feitas aos poucos e conforme for surgindo a necessidade.
Como as regras do comportamento do componente foi isolada, ficou mais simples de adicionar uma nova regra. No arquivo CourseCard.test.js
entra um novo teste para garantir a nova funcionalidade:
O novo teste irá falhar, pois ele espera pela implementação:
O novo teste passa, assim como todos os outros.
Teste de regressão
Com a nova funcionalidade implementada e todos os testes de unidade passando, é hora de executar os testes de integração novamente.
Lembre-se de interromper a execução dos testes de unidade, e depois executar a API e o client antes de executar os testes de integração.
Neste contexto, se os testes de integração estivessem com erro, seria um sinal que a aplicação regrediu, pois o que estava funcionando antes passou a não funcionar mais. Então o teste de regressão (regression testing) nada mais é que um grupo de testes que é feito para garantir que o sistema continua funcionando mesmo depois de ser modificado. Pode ser usado qualquer tipo de teste, inclusive o teste manual.
Resultado e próximos passos
Com poucos passos foi possível entregar um enorme valor. Uma nova funcionalidade foi implementada, não houve regressão, parte do código foi refatorado e agora está melhor do que antes, o projeto ganhou vários testes automatizados, tudo isso irá auxiliar o desenvolvedor a manter a qualidade do seu trabalho.
Nos próximos artigos serão abordados outros cenários de refatoração, como refatoração em componentes que dependem de outros componentes, nos ciclos de vida dos componentes, nos estilos, nos recursos do Redux, nas camadas de integração, entre outros desafios.
Deixe sua opinião sobre esta segunda parte da série. O que achou da refatoração, dos testes de unidade e da implementação da nova funcionalidade? O que você faria de diferente? Participe com seu comentário!