Mobile

8 out, 2015

Boas práticas de Cucumber e Calabash – Parte 02

Publicidade

Introdução

Quando comecei a trabalhar com projetos de QA na Concrete, eu passei a me perguntar quais seriam as melhores práticas para a automatização dos testes usando Calabash. Uma boa parte das dicas com as minhas descobertas eu publiquei neste artigo aqui, há algum tempo. Hoje, vou falar um pouco mais sobre o que eu tenho encontrado em algumas publicações na web de pessoas mais experientes na área. Em especial, este texto do Clemens Helm me motivou a escrever uma segunda parte sobre o assunto.

E, por incrível que pareça, foram as discussões nos comentários que eu considerei mais úteis: duas formas de implementar os seus testes automatizados no Calabash. Duas visões antagônicas que não necessariamente estão erradas, mas que cabe um levantamento dos prós e dos contras de cada abordagem antes que alguém decida optar por uma delas. Então vamos lá!

Como sabemos, quando utilizamos o Cucumber, os projetos de testes normalmente se dividem em duas camadas:

cucumber-1

Quando trabalhamos com Calabash, uma terceira camada se faz necessária, a de Screens:

cucumber-2

Em alguns casos, quando os steps das diferentes plataformas são muito diferentes, eles costumam ser definidos de maneira individualizada para cada plataforma. Assim, a arquitetura acima pode ficar assim:

cucumber-3

Essa é uma maneira de estruturar o seu projeto de testes para o Calabash quando estamos usando iOS e Android. Neste artigo, vamos abordar duas maneiras de como implementar os seus testes em todas as camadas acima citadas. Mas, antes, vamos deixar claras algumas nomenclaturas:

  • Vamos chamar a primeira camada como a que contém seus arquivos Feature do projeto.
  • A segunda se refere aos arquivos de Steps.
  • Já a terceira é referente à implementação das classes Screen.

Outro termo que vamos utilizar aqui é “alto nível”, que se refere ao entendimento do usuário, e “baixo nível”, relacionado ao entendimento de implementação do desenvolvedor.

Depois dessa pequena contextualização, vamos para a discussão das duas possibilidades de implementação das camadas dos nossos projetos de testes com Calabash.

Padrão 1: Detalhamento no alto nível

Conforme indica o título, essa corrente de pensamento acredita que é uma boa prática de desenvolvimento para Cucumber – arquitetura em que o Calabash se baseia – implementar os passos de arquivo .feature da maneira mais detalhada possível.

Por exemplo, suponhamos que o programador de QA resolva testar a funcionalidade de fazer login do seu aplicativo. Um possível arquivo .feature, pertencente a uma hierarquia de alto nível, sob essa perspectiva, seria um escrito da seguinte forma:
Funcionalidade: Fazer login

Cenário: Fazendo login com sucesso.

Dado que o usuário está na tela de login
Quando o usuário digitar o login válido
             E o usuário digitar a senha válida
             E clicar no botão de logar
Então o usuário estará na página de usuário logado.

 

O arquivo de steps, correspondente à segunda hierarquia de abstração, fica assim:

Dado(/^que o usuário está na tela de login $/) do
@page = page(TelaLogin).await(timout:10)
end

Quando(/^ o usuário digitar o login válido$/) do
@page.digitar_login USERS[:valid][:login]
end

Quando(/^ o usuário digitar a senha válida$/) do
@page.digitar_senha USERS[:valid][:senha]
end

Quando(/^ clicar no botao logar$/) do
@page.clicar_logar
end

Então(/^ o usuário estará na pagina de usuário logado$/) do
@page.digitar_senha
end

 

O primeiro step atribui a classe TelaLogin a uma variável. A classe TelaLogin pertence à terceira camada e representa a tela de login, tanto para Android quanto para iOS e com a mesma interface, porém com diferentes implementações para cada sistema operacional.

A implementação da classe de tela login de Android, referente à terceira camada, poderia ficar assim:

# -*- encoding : utf-8 -*-
require 'calabash-android/abase'


class WordPressComPage < Calabash::ABase
…
#definição dos id's id_campo_login, id_campo_senha, id_botao_logar
… 

def digitar_login nome
enter nome, id_campo_login
end
def digitar_senha senha
    enter senha, id_campo_senha
end
def clicar_logar
    touch("* id:'id_botao_logar'")
end

def enter text, element, query = nil
    if query.nil?
      query( "* marked:'#{element}'", {:setText => text} )
    else
      query( query, {:setText => text} )
    end
  end

 

Observe que, para cada gesto do usuário, definido na linguagem de alto nível do Gherkin na camada de features, existe um método correspondente nas classes de Screens, que corresponde à abstração da tela e às possíveis ações sobre ela.

Claro que, se formos implementar os testes para uma funcionalidade que requer muitos passos, é possível agrupá-los sob a definição de um novo passo, por meio da seguinte sintaxe:

Quando(/^ realizar um passo gigante$/) do

step('<passo 1>')
step('<passo 2>')
...
step('<passon n>')
end

 

Você pode ver mais detalhes sobre esse agrupamento neste link.

Esse recurso vai contra o que é mostrado na outra corrente de pensamento, que evita fazer agrupamento de steps e delega todos os detalhes para a 3ª camada que mencionamos no início do artigo, de implementação dos métodos da tela.

Observe que o agrupamento é formado por steps já utilizados e definidos pelo desenvolvedor. Prefira utilizar agrupamentos quando você quiser resumir um conjunto de passos que já foram testados apropriadamente em outros cenários e que não sejam o foco de atenção para um cenário específico. Com essa abordagem, o custo é somente o de manutenção: caso um dia seja necessário modificar o enunciado de um step, será necessário atualizar onde quer que ele tenha sido utilizado.

Vantagens:

  • Legibilidade para o desenvolvedor – é mais fácil saber o que cada passo representa e como é feita a correspondente implementação em cada camada;
  • Maior transparência – as partes interessadas do projeto poderão ter mais detalhes, em linguagem natural, de como seriam as ações do usuário para cada cenário de teste;
  • Reutilização de código – um step pode ser facilmente reutilizado. Basta fazer referência a ele em qualquer agrupamento de steps e deixá-lo o mais modularizado possível.

Desvantagens:

  • Muitos steps entediantes – exagerar na quantidade de passos para um cenário de teste pode ser desestimulante para leitura. Neste caso, é válido o agrupamento, conforme discutido;
  • Custo de manutenção – se um mesmo passo for utilizado em vários agrupamentos, haverá o custo de manter as referências a esse passo atualizadas, caso seu enunciado venha a ser modificado no futuro. Sugestão: agrupe os passos quando tiver certeza que não irá mais modificar o enunciado dos subpassos, para que não precise atualizá-los sempre que estes precisarem sofrer alterações. Normalmente isso requer experiência do implementador para saber como deixar um step com um enunciado que seja pouco modificado ao longo do projeto.

Agora vamos analisar outra possibilidade de implementação das camadas do projeto de testes com Calabash.

Padrão 2: detalhamento em baixo nível

Conforme já é de se esperar, nesse padrão os arquivos .feature, correspondentes à hierarquia de alto nível, são bem sucintos. Os detalhes ficarão especificados em baixo nível, mais especialmente na terceira camada – de Screens. Ou seja, os subpassos relacionados a um passo mais sucinto serão implementados nas outras camadas, principalmente na terceira.

Neste caso, os métodos das classes desta última camada ficariam eventualmente grandes, pois representariam todos os passos de um único step, resumindo uma grande quantidade de subpassos.

Nesse modelo, não se recomenda sequer o agrupamento de <em>steps</em> para auxiliar. Vamos retomar o mesmo exemplo da seção anterior. Nossa .feature ficaria assim:
Funcionalidade: Fazer login

Cenário: Fazendo login com sucesso.

Dado que o usuário está na tela de login
Quando o usuário digitar as credenciais válidas
Então o usuário estará na página de usuário logado.

 

A definição dos steps poderia ficar algo do tipo:

Dado(/^que o usuário está na tela de login $/) do
@page = page(TelaLogin).await(timout:10)
end

Quando(/^o usuário digitar as credenciais válidas $/) do
@page.fazer_login USERS[:valid]
end

Então(/^ o usuário estará na pagina de usuário logado$/) do
@page.digitar_senha
end

 

Nesse caso, utilizamos um hash que demos o nome de “USERS”, externo ao arquivo de steps, para armazenar os dados que queremos averiguar. Na definição das telas, veríamos a maior diferença em relação ao método anterior: removemos os métodos “digitar_login”, “digitar_senha” e “clicar_logar” e criamos um método mais completo, o “fazer_login”, que englobaria todos eles:

def fazer_login user
enter user[:nome], id_campo_login
    enter user[:senha], id_campo_senha
    touch("* id:'id_botao_logar'")

 

Observe que este é um caso simples, porém pode haver passos que requeiram muito mais subpassos ou verificações implícitos, nos quais as funções podem ficar grandes e complexas. Essa abordagem é a utilizada pela Xamarin, cujo exemplo mais completo pode ser achado neste repositório.

Vantagens:

  • Garante total desacoplamento entre a implementação das classes de tela e os passos correspondentes. Por exemplo, se fizermos login no iOS usando um procedimento X que é totalmente diferente do procedimento Y do Android, a primeira (features) e a segunda (steps) camadas não precisam se preocupar tanto com a diferença semântica entre plataformas. Somente a terceira camada precisaria tratar essas diferenças, naturalmente por ser a camada relacionada às especificidades de implementação de cada plataforma;
  • Facilidade em trabalhar quando existem muitas funcionalidades que agem de maneiras completamente distintas entre as duas plataformas: quando um mesmo cenário requer a realização de steps de maneiras distintas em diferentes sistemas operacionais (Android, iOS, Windows Phone, entre outros).

Desvantagens:

  • Perda de legibilidade – os subpassos de um passo, quando em grande quantidade e implementados dentro das classes de telas (3ª camada), ficam difíceis de ler;
  • Ofuscabilidade – as partes interessadas no projeto não terão conhecimento das ações do usuário em detalhes, a não ser que eles leiam as implementações das Screens, o que não é desejado.

Conclusão

Não existe a maneira certa de implementar os seus testes. Tanto priorizar os detalhes nas camadas de alto nível (primeira e segunda) quanto priorizar os detalhes nas camadas de baixo nível (segunda e terceira) têm vantagens e desvantagens.

No último projeto em que trabalhei na Concrete, eu achei melhor seguir o primeiro padrão. Por outro lado, a Xamarin, com toda a sua experiência, sugere utilizar o segundo padrão. Eu pessoalmente me sentiria perdido tendo que agrupar um monte de passos em um único método de uma classe de Screen. Se eu tivesse algum passo repetido para outro cenário, teria praticamente que reescrever tudo!

Acredito que o segundo padrão seja mais útil quando os passos são realizados de maneira distinta entre as diferentes plataformas mobile. Ainda assim, nós da Concrete concebemos aquele padrão de camadas já citadas no começo deste artigo:

cucumber-3

Pois, assim, podemos utilizar o primeiro padrão, caso a preferência seja deixar as diferenças entre as plataformas nas camadas de Feature ou de Steps.

E aí, já trabalhou com testes automatizados antes? Qual abordagem você se identificou mais e por quê? Mande seus comentários!

Fontes de inspiração:

Até a próxima!