Organização de testes de aceitação com PageObjects

Testes de aceitação são extremamente úteis quando se trata de verificar se as funcionalidades de um sistema estão se comportando corretamente sem que tenhamos de testar manualmente a aplicação, abrindo um navegador, navegando por ela e visualizar os resultados. Como poderíamos automatizar esses testes que envolvem realizar a navegação na aplicação?

Para isso ser possível, existem algumas ferramentas que permitem tal trabalho, simulando a interação de um usuário com a aplicação. Esse é o caso do Selenium que, com a assistência do JUnit, pode facilitar o trabalho de escrever tais testes.

Para vermos como o Selenium pode nos ajudar, vamos considerar uma página contendo simples HTML, que ao clicarmos em um link dela, seremos redirecionados para outra página.


 <a class="botao" href="outra_pagina.html">Clique Aqui</a>

Quando clicado, o link deve redirecionar para a outra_pagina.html, que tem o conteúdo:

</pre>
<h2>Estou na outra página!</h2>
<pre>

Com essas páginas, podemos escrever um teste, usando o Selenium, para que ele clique no link e em seguida verifique se estamos na outra página. Para isso, podemos escrever um pequeno teste com o JUnit usando a API do Selenium:

@Test
public void deveIrParaAOutraPagina() {
   WebDriver driver = new FirefoxDriver();
   driver.navigate().to("http://localhost:8080/pagina.html");

   driver.findElement(By.className("botao")).click();

   String texto = driver.findElement(By.tagName("h2")).getText();
   assertEquals("Estou na outra página!", texto);
}

Com a API do Selenium, esse código parece simples, não é? Mas imagine se precisássemos fazer várias ações e navegar por diversas páginas para conseguirmos testar uma funcionalidade. Um possível exemplo seria o login aplicação, onde uma possibilidade de teste é:

@Test
public void aoSeLogarNaAplicacaoDeveMostrarQueEstaLogado(){
   WebDriver driver = new FirefoxDriver();
   driver.navigate().to("http://localhost:8080/login.html");

   // pega uma referencia para o formulário de login que estará na tela
   WebElement formulario = driver.findElement(By.tagName("form"));

   // preenche usuário e senha
   driver.findElement(By.id("usuario")).sendKeys("joao");
   driver.findElement(By.id("senha")).sendKeys("123456");

   // submete o formulário
   formulario.submit();

   // na próxima tela, o login desse usuário deveria estar aparecendo
   String usuarioLogado = driver.findElement(By.id("usuario-logado")).getText();

   // verifica se o joao conseguiu fazer o login
   assertEquals("joao", usuarioLogado);
}

Apesar de funcionar perfeitamente, o código acima possui um grave problema: lê-lo já está extremamente mais complexo do que o teste que havíamos feito antes! Agora, imagine se estivéssemos escrevendo testes para uma funcionalidade que envolva interação com várias outras telas?

Para simplificar e organizar esse código de teste, vamos separar cada uma das telas em classes diferentes, ou seja, para a tela de login vamos criar uma classe chamada TelaDeLogin e para a página interna uma classe chamada PaginaInterna, dessa forma poderíamos ter o método loga com seguinte código para a TelaDeLogin:

public class TelaDeLogin {

   public void abre() {
      driver.navigate().to("http://localhost:8080/login.html");
   }

   public void loga(String usuario, String senha) {
      // pega uma referencia para o formulário na tela
      WebElement formulario = driver.findElement(By.tagName("form"));

      // preenche usuário e senha
      driver.findElement(By.id("usuario")).sendKeys(usuario);
      driver.findElement(By.id("senha")).sendKeys(senha);

      // submete o formulário
      formulario.submit();
   }
}

Note que o código acima ainda não compila, pois precisamos de uma referência para o WebDriver. Podemos passar a recebê-la via construtor:

public class TelaDeLogin {
   private WebDriver driver;

   public TelaDeLogin(WebDriver driver) {
      this.driver = driver;
   }

   public void abre() {
      // navega para a tela de login
   }

   public void loga(String usuario, String senha) {
      // código que usa o Selenium para fazer o login
   }
}

Dessa forma, podemos usar a nova classe TelaDeLogin no lugar do código do Selenium em nossas classes de teste. Com isso, o teste ficará parecido com:

public class TestesDeAceitacao extends SeleniumTest {

   private TelaDeLogin telaDeLogin;

   // Instanciamos a base de nossa aplicacao
   @Before
   public void setup() {
      WebDriver driver = new FirefoxDriver();
      this.telaDeLogin = new TelaDeLogin(driver);
   }

   // Efetuamos o teste
   @Test
   public void aoSeLogarNaAplicacaoDeveMostrarQueEstaLogado(){
      telaDeLogin.abre();
      telaDeLogin.loga("joao","123456");

      // na próxima tela, o login desse usuário deveria estar aparecendo
      String usuarioLogado = driver.findElement(By.id("usuario-logado")).getText();

      // verifica se o joao conseguiu fazer o login
      assertEquals("joao", usuarioLogado);
   }
}

O código do teste melhorou, mas ainda continua com problemas, um deles é que ainda usamos a API do Selenium diretamente dentro do teste e novamente, se ele fosse um pouco mais complexo, continuaria difícil de ler seu código. Como o trecho que ainda está usando o código do Selenium direto no teste diz respeito à página interna do sistema, podemos levar esse código para a classe PaginaInterna, dessa forma teremos:

public class PaginaInterna {
   private WebDriver driver;

   public PaginaInterna(WebDriver driver) {
      this.driver = driver;
   }

   public boolean estaLogado(String usuario) {
      String usuarioLogado = driver.findElement(By.id("usuario-logado")).getText();

      return usuarioLogado.equals(usuario);
   }
}

Dessa maneira, podemos fazer uma simples refatoração no nosso teste, para usar a nova classe implementada:

   @Test
   public void aoSeLogarNaAplicacaoDeveMostrarQueEstaLogado() {
      String usuario = "joao";
      telaDeLogin.abre();
      telaDeLogin.loga(usuario, "123456");

      boolean estaLogado = new PaginaInterna(driver).estaLogado(usuario);
      assertTrue(estaLogado);
   }

Agora estamos num nível melhor, mas continua tendo pontos para melhorar. O principal deles agora, é que toda troca de tela que existir em nosso teste, precisamos instanciar um objeto para a nova tela, passar o driver como parâmetro, para aí sim, podermos invocar o método. Note que ao fazermos o login com sucesso, nesse caso, sempre seremos redirecionados para a mesma página, sendo assim, poderíamos fazer o método loga já retornar uma instância de PaginaInterna para nós e assim, só precisaríamos chamar o método verificaSeEstaLogado:

public class TelaDeLogin {

   // construtor e atributo driver

   public PaginaInterna loga(String usuario, String senha) {
      // pega uma referencia para o formulário na tela
      WebElement formulario = driver.findElement(By.tagName("form"));

      // preenche usuário e senha
      driver.findElement(By.id("usuario")).sendKeys(usuario);
      driver.findElement(By.id("senha")).sendKeys(senha);

      // submete o formulário
      formulario.submit();

      return new PaginaInterna(driver);
   }
}

Essa alteração na classe TelaDeLogin nos permite encadear as chamadas dos métodos loga e verificaSeEstaLogado no nosso teste de aceitação:

   @Test
   public void aoSeLogarNaAplicacaoDeveMostrarQueEstaLogado() {
      String usuario = "joao";
      telaDeLogin.abre();

      boolean estaLogado = telaDeLogin.loga(usuario, "123456").estaLogado(usuario);
      assertTrue(estaLogado);
   }

Agora nosso teste está bem mais legível, e o código do Selenium todo encapsulado dentro de classes, que chamamos de PageObjects. Ainda há brechas que precisam de melhoras. Podemos fazer o método abre() retornar a própria instância de TelaDeLogin e, com isso, poderíamos ter todos os métodos encadeados, o que faria o teste ficar como:

   @Test
   public void aoSeLogarNaAplicacaoDeveMostrarQueEstaLogado(){
      String usuario = "joao";

      boolean estaLogado = telaDeLogin.abre().loga(usuario, "123456").estaLogado(usuario);
      assertTrue(estaLogado);
   }

Com isso, temos um código de teste bem mais legível, onde toda a API do Selenium está encapsulado dentro de classes que representam as páginas, formando os Page Objects.

Que outras ferramentas e práticas você utiliza para facilitar seus testes de aceitação?

13 Comentários

  1. Raphael 09/04/2012 at 12:00 #

    bemm bacana! ja queria blogar sobre isso ha um tempo ja!

    congrats!

  2. Allan 09/04/2012 at 13:58 #

    SUCESSO Leozin, Parabéns!!!

  3. Felipe Affonso 09/04/2012 at 16:00 #

    Sensacional o post. A cada dia mais cresce meu interesse por testes. Tenho visto no dia-a-dia o ganho em qualidade, em vários aspectos.

    Parabéns!

  4. Ivan Rodrigues 09/04/2012 at 17:14 #

    Pois é Felipe Affonso, o pior é que muito se vê no mercado os profissionais mais interessados em qualidade do que a gerencia, tudo por causa do custo. Mesmo com os mais sensacionais frameworks e métodos para teste, não tem para onde correr, provavelmente 90% da galera vai usar 5% do que estuda rsrs….

  5. Luciano Molinari 10/04/2012 at 08:10 #

    Muito legal o artigo, parabéns

  6. Paulo Mariano 17/09/2013 at 09:04 #

    Muito bom post Leonardo.
    Estou usando um framework para facilitar o uso de PageObjects e testes no selenium, se chama FuncaoWeb (disponivel em: https://github.com/confsoft/FuncaoWeb).
    Mas tenho algumas questões que não consegui ainda entender como usar o PageObjects. E se dependendo do login o usuário é direcionado há diferentes páginas? O que fazer? Qual classe vou retornar?

  7. Daniel Carvalho 16/12/2013 at 10:17 #

    @Paulo Mariano, eu estou usando há algum tempo o padrão. Eu tenho algo parecido com seu problema. Na verdade é uma tela de certificado que pode aparecer ou não. Então eu fiz uma classe utilitário, chamei de LoginService, com um método logar… nesse método eu trato as exceções. Se aparecer a tela de certificado eu sigo um fluxo, se lançar uma exceção por não ser a tela de certificado eu faço outro fluxo, e assim por diante. Espero ter ajudado.

  8. R. Alves Pinheiro 26/01/2015 at 17:18 #

    No caso de uma autenticação do browser ?, exemplo quando rodo o teste (browser), geralmente pede um autenticação (autenticação solicitada) , onde trabalho o browser sempre abre na pagina da intranet da empresa, e temos que se logar.

  9. Leonardo Wolter 26/01/2015 at 17:23 #

    Olá

    Nesse caso você poderia ter um Page Object PaginaDeLogin onde você teria o método loga() que preenche o formulário e devolve um Page Object PaginaInicial, por exemplo.

    Abraço

  10. Danubia Paiva 22/06/2016 at 10:24 #

    Bom dia Leonardo,

    Estou iniciando em testes automatizados e gostaria de uma opinião. Qual a melhor forma para estruturar os testes? Criar todos os testes em uma unica classe, ou uma classe para cada teste? Ex: Uma classe para cada item do menu (inclui, exclui…), ou uma classe para todos os menus.

  11. Valter Negreiros 03/11/2016 at 16:23 #

    Danubia, a minha recomendação é criar uma classe por módulo do sistema.
    Uma classe por CRUD de Tela.

Deixe uma resposta