Code smells

Code Smell – Duplicação de Código

Hoje, vamos ver em Ruby como combater o Code Smell de duplicação de códigos usando um design pattern conhecido como Template Method. Identificando o Code Smell Um cenário muito comum ao desenvolvermos software é nos depararmos com a seguinte situação: uma quantidade significativa de código que resolve um problema X, mas que precisa sofrer pequenas alterações […]

Background Image

Hoje, vamos ver em Ruby como combater o Code Smell de duplicação de códigos usando um design pattern conhecido como Template Method.

Identificando o Code Smell

Um cenário muito comum ao desenvolvermos software é nos depararmos com a seguinte situação: uma quantidade significativa de código que resolve um problema X, mas que precisa sofrer pequenas alterações para resolver também o problema Y, Z e “n” outros problemas muito parecidos entre si.

Vamos supor que você esteja trabalhando em um projeto que precisa gerar boletos bancários para diversos bancos.

Ao olhar o código do sistema em produção, você percebe que  algum desenvolvedor no passado (que já saiu da empresa, afinal ninguém quer assumir a culpa) resolveu brilhantemente isolar o código para gerar boletos bancários para o Itaú em uma classe chamada BoletoItau – classe essa que parece com a seguinte:

class BoletoItau
  def initialize(valor, vencimento)
    @valor = valor
    @vencimento = vencimento
  end
  
  def gerar_boleto
    puts("Header do Boleto para o Banco Itau")
    puts("Valor: #{@valor}")
    puts("Vencimento: #{@vencimento}")
    puts("Footer do Boleto: Esse boleto deve ser pago no Banco Itau mais próximo")
  end
end

Parabéns para o desenvolvedor que resolveu isolar o código acima em uma classe. Mas, quando esse mesmo desenvolvedor teve que gerar um boleto para o banco Santander (que parece muito com o boleto do Itaú) ele fez algo muito, muito ruim: ele duplicou a classe BoletoItau, criando a BoletoSantander:

class BoletoSantander
  def initialize(valor, vencimento)
    @valor = valor
    @vencimento = vencimento
  end

  def gerar_boleto
    puts("Header do Boleto para o Banco Santander")
    puts("Valor: #{@valor}")
    puts("Vencimento: #{@vencimento}")
    puts("Footer do Boleto: Esse boleto deve ser pago no Banco Santander mais próximo")
  end
end

Agora você, que esta com a tarefa para criar boletos para o Banco do Brasil, se vê diante das opções: perpetuar esse legado de código duplicado, ou refatorar o código acima para algum design pattern existente.

Não se deixe seduzir pelo lado negro da força, gaste um tempinho e resolva permanentemente esse problema com o Template Method.

Identificando o que permanece igual

A chave central do Code Smell de código duplicado é identificar o que se repete entre os códigos semelhantes e isolá-los em uma classe que servirá de base para todas as subclasses.  Uma definição formal do pattern Template seria:

A idéia geral do Pattern Template Method é criar uma classe base abstrata com um método que sirva de esqueleto. Esse método possui o processamento que pode variar, realizando chamadas a métodos abstratos, que serão fornecidos por subclasses concretas.

Fonte: Design Patterns in Ruby

No exemplo anterior, vemos que as principais diferenças entre as classes BoletoItau e BoletoSantander se encontra no método gerar_boleto. 

Para transformar o método gerar_boleto em um Template Method, eu preciso antes transformar o código que ele executa em chamadas as métodos que serão sobrescritos por cada subclasse, que no meu exemplo, será cada Banco.

Uma proposta da classe Boleto seria:

class Boleto
  def initialize(valor, vencimento)
    @valor = valor
    @vencimento = vencimento
  end

  def gerar_boleto
    gerar_header
    gerar_valor
    gerar_vencimento
    gerar_footer
  end

  def gerar_header
  end

  def gerar_valor
    puts("Valor: #{@valor}")
  end

  def gerar_vencimento
    puts("Vencimento: #{@vencimento}")
  end

  def gerar_footer
  end
end

Com isso, a cada novo Banco, eu posso simplesmente estender a classe base Boleto e sobrescrever os métodos desejados. Os bancos poderiam ficar da seguinte forma:

# Banco Itaú
class BoletoItau < Boleto
  def gerar_header
    puts("Header do Boleto para o Banco Itau")
  end

  def gerar_footer
    puts("Footer do Boleto : Esse boleto deve ser pago no Banco Itau mais proximo")
  end
end

# Banco Santander
class BoletoSantander < Boleto
  def gerar_header
    puts("Header do Boleto para o Banco Santander")
  end

  def gerar_footer
    puts("Footer do Boleto : Esse boleto deve ser pago no Banco Santander mais proximo")
  end
end

# E finalmente o Banco do Brasil
class BoletoBancoBrasil < Boleto
  def gerar_header
    puts("Header do Boleto para o Banco do Brasil")
  end

  def gerar_footer
    puts("Footer do Boleto : Esse boleto deve ser pago no Banco do Brasil mais proximo")
  end
end

Don’t Repeat Yourself (DRY)

Code Smell
Photo by cottonbro studio on Pexels.com

A metodologia Don’t Repeat Yourself (DRY) é uma filosofia de programação que busca evitar a duplicação de código.

A ideia principal do DRY é que cada pedaço de conhecimento na programação deve ser representado por uma única, isto é, um única “entidade autoritativa”.

A implementação do princípio DRY pode ajudar a reduzir a complexidade e aumentar a manutenibilidade do código, pois elimina a necessidade de manter várias cópias do mesmo código, que podem acabar divergindo umas das outras.

Portanto, se qualquer parte do código precisar ser modificada, apenas precisará ser feito em uma única lugar, em vez de em vários lugares.

Por exemplo, algumas técnicas comuns para implementar o princípio DRY incluem:

  • Utilizar funções e procedimentos para encapsular lógicas comuns
  • Utilizar herança e polimorfismo para compartilhar comportamento entre classes
  • Utilizar bibliotecas e frameworks que forneçam funcionalidades comuns
  • Utilizar código genérico para evitar a criação de código específico para cada caso
  • Utilizar arquitetura modular para dividir seu código em módulos independentes e reutilizáveis

DRY é importante porque o código duplicado é uma fonte comum de problemas de manutenção e pode levar a bugs difíceis de encontrar.

Ao seguir o princípio DRY, você pode aumentar a confiabilidade do seu código para então torná-lo mais fácil de manter e evoluir no futuro.

Conclusão

O exemplo foi bastante reduzido para não tornar este post em uma dissertação acadêmica, mas adianto que  existe um mar de técnicas que podem ser aplicadas para evitar o Code Smell de duplicação de código.

Caso queira uma recomendação de livro, indico o Design Patterns – Elements of Reusable Object-Oriented Software, que é um livro mais abrangente sobre o tema.

Caso queria algo mais prático, tem o excelente Refactoring to Patterns que é um livro mais prático sobre Patterns. Em Ruby, existe o Design Patterns in Ruby, que usei como base para este post – e possivelmente usarei como base para os próximos posts.

Por fim, deixo também uma sugestão do clássico Clean Code, do Robert C Martin.

Page objects

Testes funcionais com Cucumber e Page Objects

Hoje vamos criar testes funcionais usando Cucumber e mais algumas libs do mundo Ruby. Anteriormente, meu post foi justamente uma tradução do ótimo artigo do Martin Fowler sobre  Page Objects – e agora vou engatar no tema, postando algumas sugestões de como usar esse pattern. Vamos lá! Ingredientes Assim, a idéia é construir o seguinte […]

Background Image

Hoje vamos criar testes funcionais usando Cucumber e mais algumas libs do mundo Ruby.

Anteriormente, meu post foi justamente uma tradução do ótimo artigo do Martin Fowler sobre  Page Objects – e agora vou engatar no tema, postando algumas sugestões de como usar esse pattern. Vamos lá!

Ingredientes

Assim, a idéia é construir o seguinte ambiente:

  1. Testes no cucumber  : consomem suas pages.
  2. Suas pages: definem seus page-objects, que encapsulam chamadas ao seu driver
  3. Watir-webdriver : Manipula chamadas ao Watir
  4. Watir : Manipula o browser, que no meu exemplo será uma instância do Firefox.

O mundo real e os testes funcionais

person encoding in laptop
Photo by Lukas on Pexels.com

Testes funcionais são, por natureza, difíceis de manter. Isso  porque além deles serem sensíveis a mudanças do seu código server-side, eles são sensíveis a mudanças na interface da sua aplicação web (e portanto ao código client-side).

Logo, por exemplo, se o programador front-end mudar o id de algum elemento importante para os seus testes, ou construir um código javascript que obstrua o carregamento da página (no caso de uma requisição ajax para montar um bloco HTML), seus testes facilmente irão falhar.

Além disso, as interfaces web hoje em dia estão bem difíceis de serem testadas. Já eram na “Era Flash” e suas complicações com Selenium e embora as interfaces estejam cada vez mais javascript-based, você ainda tem complicações devido as diversas requisições ajax e “mágicas javascript”, que tendem a dar algumas dores de cabeça ao serem testadas por um driver web.

Proposta dos testes funcionais

Pretendo aqui montar um projeto exemplo de testes funcionais, testando uma busca no meu Blog.

Eu aconselho você, no seu projeto real, manter os testes funcionais (com cucumber e page objects) em um projeto separado da aplicação a ser testada, ainda que esta seja em Ruby também

Isso é bom, principalmente por que você isola as dependências das bibliotecas peculiares a cada projeto (da aplicação a ser testada e dos testes).

Recentemente tive que criar testes funcionais em uma aplicação web Scala/Lift, e manter o projeto de functional tests separado foi fundamental para isolar as dependências J2EE/Scala do projeto de testes em Ruby/Cucumber.

Certamente, além de facilitar o isolamento dos jobs na pipeline do servidor de CI, que no meu caso foi o Jenkins. Caso alguém se manifeste a favor, posso postar futuramente como configurar um job no jenkins para testes funcionais em Ruby/Cucumber.

Passo 1 : Defina suas Páginas

Uma forma de começarmos a testar é definir antes suas pages e então definir seus objects. Não só a gem page-object te permite fazer isso, como também tem uma DSL simples e intuitiva.

Neste projeto exemplo, eu criei a seguinte Page:

# encoding: UTF-8
require 'page-object'
require_relative '../../../config/env'

class HomePage
  include PageObject
  page_url "http://www.blogdopedro.net/"
  expected_title "Pedro Mendes - Desenvolvimento web e um pouco mais."

  # Page Objects
  text_field :input_search, :name => "s"
  button :search, :class => "search-submit"
end

Como você pode ver, eu defini os page  objects usando uma DSL que me remete aos próprios elementos HTMLs. Para cada page object, a lib ruby “page-object” cria diversos métodos “mágicos” que auxiliam nos testes.

Por exemplo:

page.text_field # retorna o valor do campo de text ou (se for um link ou button), clica no element
page.text_field= # Atribuí um valor
page.text_field_element # Retorna o próprio elemento

Existem diversos outros métodos, que você pode encontrar diretamente na documentação da gem.

Passo 2 : Defina suas Features

Essa parte é mais fácil, basta seguir o padrão já estabelecido pelo próprio Cucumber, sem grandes mudanças.

Feature: Search For PageObject Article
As a Blog do Pedro visitor
I want to find for PageObjects article
Scenario:
Given I am on Blog do Pedro's Home Page
When I input a term into search text field
Then I should submit search form

Minha única dica nesse ponto é adicionar um sufixo numérico no nome do arquivo físico da sua feature (ex. 001_minha_feature.rb). Frequentemente seus testes funcionais precisam seguir uma sequência pré-estabelecida  (e só encontrei essa forma no Cucumber para ele respeitar alguma ordem).

Exemplo:

  • 001_cadastrar_usuario.rb
  • 002_ativar_usuario_cadastrado.rb
  • 003_acessar_conta_como_usuario.rb

Passo 3 : Defina seus steps usando suas pages (e seus page-objects)

Usando as gems watir_webdriver e a page object, ganhamos alguns métodos importantes nos nossos steps. Um deles é o visit_page, que aponta o browser para a url definida na Page – geralmente usado no início dos testes. Outro item importante: você pode ter várias pages com a mesma url, caso queria testar uma página que tenha comportamentos diferentes.

No projeto-exemplo, os steps para a HomePage ficaram:

# encoding: UTF-8

Given(/^I am on Blog do Pedro's Home Page$/) do
  visit_page HomePage
end

When(/^I input a term into search text field$/) do
  on_page HomePage do |page|
    page.input_search = "Tradução do artigo sobre PageObjects"
  end
end

Then(/^I should submit search form$/) do
  on_page HomePage do |page|
    page.search
  end
end

Deixo duas dicas para esse passo: crie um arquivo (ou alguns) para passos que se repitam em diversas features.

É comum, por exemplo, nos testes funcionais de uma aplicação que tenha algum tipo de permissionamento baseado em papéis (ACL), que você tenha que se autenticar na aplicação diversas vezes, com usuários diferentes. Para isso, coloque todos os steps necessários para fazer  o login em um arquivo só. Como os arquivos que definem os steps são “compartilhados” por todas as features, todos os steps são públicos a todas as features.

A segunda dica é : cuidado com os waitings explícitos. É comum, graças a problemas com timeouts, você colocar algum tipo de wait na página.

Por exemplo:

on_page HomePage do |page|
  page.input_search_element.when_visible(30)
  page.input_search = "Meu termo de busca"
end

Ou mesmo:


on_page HomePage do |page|
  sleep(30)
  page.input_search = "Meu termo de busca"
end

Geralmente, testes com Selenium possuiam muitos waitings explícitos – e isso era um grande causador de lentidão nos testes, que demoravam horas para rodarem. É óbvio que os waits explícitos às vezes são a única saída para lidar com algum timeout, mas evite sempre que possível.

Conclusão

Caso você baixe o projeto-exemplo, ao rodar no seu terminal você deverá ver algo parecido com isso:


$ rake cucumber
/Users/pedrolopesme/.rvm/rubies/ruby-1.9.3-p448/bin/ruby -S bundle exec cucumber --profile default
Using the default profile...
Feature: Search For PageObject Article
As a Blog do Pedro visitor
I want to find for PageObjects article
Scenario: # features/001_search_for_pageobject_article.feature:6
Given I am on Blog do Pedro's Home Page # features/steps_definitions/001_search_for_pageobject_article.rb:3
When I input a term into search text field # features/steps_definitions/001_search_for_pageobject_article.rb:7
Then I should submit search form # features/steps_definitions/001_search_for_pageobject_article.rb:13
1 scenario (1 passed)
3 steps (3 passed)
0m8.384s

Testes funcionais são custosos, mas são ótimos para capturar erros causados por algum problema na interface ou que não estejam necessáriamente ligados ao seu código (problemas de rede, por exemplo).

Por fim, deixo três conselhos para aqueles que tenham algum tipo de servidor de Continuous Integration ou mesmo Continuous Deployment:

  • Na sua pipeline de testes, rode os testes unitários antes dos testes funcionais.
  •  Rode os testes funcionais em uma aplicação “deployada” em um ambiente isolado do ambiente de produção, mas que seja o mais parecido possível com o mesmo.
  • Rode os testes funcionais em um ambiente gráfico virtual, tal como o XVFB

Deixei uma cópia do código usado nesse projeto no meu github .