Page Object: a chave para tornar seus testes de aceitação mais organizados!

Imagem principal do post

Introdução

Automatizar testes de software tem se tornado quase que uma regra geral no desenvolvimento de aplicações. Os testes de aceitação não fogem a essa regra – no post anterior mostrei como você pode automatizar os testes de aceitação de uma aplicação web utilizando o Selenium WebDriver. Cada vez mais e mais estamos escrevendo e nos deparando com testes automatizados de interface – o que é muito bom. Mas se há uma certeza na Engenharia de Software é que código escrito é código que vai mudar. Ou seja, esse código vai precisar, em algum momento, ser mantido: sofrerá alterações, correções etc. Precisamos, portanto, de uma forma de organizar melhor esse código. O padrão Page Object é uma forma de fazer isso.

Este post vai mostrar como utilizar o padrão Page Object para organizar melhor o código de teste de aceitação de sua aplicação web.

Ainda não sabe como automatizar os testes de aceitação de sua aplicação web? Clique aqui e veja como!

A mudança é inevitável

Como foi mostrado no post anterior, o código utilizando a API do Selenium WebDriver manipula diretamente o HTML da página: encontra elementos, aplica ações etc. Um código que manipula diretamente o HTML é um código que está bastante sujeito a modificação. Essas modificações podem decorrer de:

  • Modificações de requisitos. Nesse caso, uma modificação na regra de negócio pode resultar em uma mudança em uma determinada página HTML: adicionando ou removendo novos elementos, alterando mensagens de aviso ao usuário etc. Nesses casos, necessariamente, o código de teste que manipula a página modificada também terá que mudar.
  • Modificações de usabilidade/design de interface. Imagine que em uma determinada página, por questões de usabilidade, decide-se trocar a forma como uma caixa de seleção foi implementada. Antes ela era implementada por meio de um select HTML; na modificação, a caixa de seleção passa a ser implementada por meio de um div HTML. Essa mudança afeta diretamente um código que automatize uma escolha nesta caixa de seleção; logo, modificação inevitável nesse código.
  • Evolução/troca de tecnologia. A aplicação utilizava como tecnologia de implementação na camada de visão o JSF. Agora vai passar a utilizar Bootstrap + AngularJS. Com isso, o código das páginas vai mudar consideravelmente. O código que automatiza os testes tem que acompanhar essas mudanças.

Portanto o código que realiza a automação de testes de interface é muito sensível à mudança. Quem trabalha com aplicações Web sabe que mudanças em telas é uma realidade permanente. Precisamos de uma solução para o seu problema.

Page Object: trate seu código de teste como você trata o seu código de produção

Quando estamos escrevendo código para a nossa aplicação – o código que vai para a produção – sempre buscamos usar as melhores práticas de Engenharia de Software. No caso da orientação a objeto, procuramos utilizar práticas como reuso, componentização, design patterns, separação de responsabilidades etc. Um dos objetivos de utilizar essas boas práticas é tornar tanto a evolução quanto a manutenção do software mais fácil.

Assim como no nosso código de produção, também temos de cuidar bem do nosso código de testes. Afinal ele também vai precisar ser evoluído e mantido – e queremos que isso seja o menos trabalhoso possível. Nos casos de testes de aceitação automatizados, que testam interfaces do usuário, podemos utilizar o Page Object para atingir esse objetivo.

Page Object é um padrão que funciona como interface de acesso a elementos da camada de visão – para aplicações web ele representa uma página HTML. Ele é aplicado para abstrair as páginas de uma aplicação com o objetivo de reduzir o acoplamento entre os casos de teste e a aplicação a ser testada.

Vantagens de se utilizar o Page Object

O seu uso traz as seguintes vantagens:

  1. Encapsulamento: o acesso a elementos da página fica encapsulado dentro do Page Object. O que ele nos fornece são métodos para pedirmos que ele realize ações sobre a página a ser testada.
  2. Abstração: para o código de teste não deve importar se no momento de fazer o login em um formulário do tipo usuário/senha, o preenchimento dos campos é feito por meio de um findElement(By.name("")) ou findElement(By.id("")). O código de teste simplesmente deveria informar o usuário/senha e esperar que a autenticação seja feita com sucesso ou não. Detalhes da implementação – que podem vir a mudar – são abstraídos pelo Page Object.
  3. Separação de Responsabilidades: o código de testes não deve ser responsável por fazer uso da API do WebDriver. Não é responsabilidade dele conhecê-la. Um código típico de teste segue um padrão comum: preparação dos dados; execução/chamada do código a ser verificado; e verificação do resultado dessa execução (asserções). Com o uso do Page Object a separação torna-se nítida: o código de teste cuida exclusivamente dos testes; o Page Object é quem sabe como acessar os elementos e realizar ações em determinada página.

Vamos mostrar um exemplo prático de código para tornar mais claro o que é o Page Object e qual a sua utilidade.

Primeiro: código de teste sem o uso do Page Object

Para o nosso exemplo, vamos utilizar uma aplicação web que escrevi e que se encontra em meu GitHub. É uma aplicação conceito de uma ferramenta de gerenciamento de casos de teste. Ela foi implementada utilizando AngularJS e Spring MVC.

Nota: essa aplicação utiliza como base de dados um banco HSQLDB que reside na memória. Para que os testes funcionem é necessário:

  1. colocar a aplicação em um servidor web (por exemplo, o Apache Tomcat)
  2. executar o script ant startDB.xml (que se encontra na raiz do projeto) para iniciar o banco de dados.

Vamos testar o cenário de autenticação com sucesso. A autenticação é feita por meio de um formulário HTML do tipo usuário/senha; sendo feita com sucesso, o usuário é redirecionado para a página inicial da aplicação – a página de listagem de projetos (veja as figuras a seguir).

Tela de login na aplicação
Tela de login na aplicação
Página inicial da aplicação - listagem de projetos
Página inicial da aplicação – listagem de projetos

O código abaixo mostra o caso de teste de autenticação com sucesso.

public class LoginSistemaSemPageObjectTest {

    private WebDriver driver;

    @Before
    public void before() {
      driver = new ChromeDriver();
    }

    @Test
    public void loginComSucesso() {
      // Acessa a página inicial que nos redireciona para a página de login
      driver.get("http://localhost:8080/simpletests");

      // Informa o usuário/senha e clica no botão de login
      driver.findElement(By.name("username")).sendKeys("joao@mpf.mp.br");
      driver.findElement(By.name("password")).sendKeys("123456");
      driver.findElement(By.cssSelector("input[type='submit'")).click();

      // Se a página inicial foi acessada: a barra de navegação existe e ela
      // é a página de projetos
      assertNotNull(driver.findElement(By.className("navbar")));
      assertThat(driver.findElement(By.tagName("h2")).getText(), containsString("Projetos"));
    }

    @After
    public void after() {
      driver.quit();
    }

}

Observe que o código anterior funciona perfeitamente. Ao executar este caso de teste com o JUnit ele passa – a barra fica verde. No entanto, o acoplamento do código de teste com a API do WebDriver não é uma boa prática; não há encapsulamento, nem abstração, nem separação de responsabilidades: o código de teste sabe demais sobre a página de teste e ele não apenas faz o teste – ele acessa a página, encontra elementos, preenche dados e faz a verificação. É muita coisa para um único método.

Isso pode ficar melhor!

Código de teste com Page Object

Vamos agora adicionar Page Objects ao nosso código de teste. No nosso caso de teste acessamos duas páginas: a página de login e página inicial da aplicação. Teremos, então, 2 Page Objects: o LoginPage e a ListaProjetosPage.

Abaixo está o código do LoginPage.

public class LoginPage {

 private WebDriver driver;

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

 public LoginPage visita(String url) {
   driver.get(url);
   return this;
 }

 public ListaProjetosPage autentica(String usuario, String senha) {
   driver.findElement(By.name("username")).sendKeys(usuario);
   driver.findElement(By.name("password")).sendKeys(senha);
   driver.findElement(By.cssSelector("input[type='submit'")).click();

   return new ListaProjetosPage(driver);
 }

}

E aqui está o código do ListaProjetosPage.

public class ListaProjetosPage {

 private WebDriver driver;

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

 public boolean isValida() {
   return temBarraNavegacao() && temListagemProjetos();
 }

 private boolean temBarraNavegacao() {
   return driver.findElement(By.className("navbar")) != null;
 }

 private boolean temListagemProjetos() {
   return driver.findElement(By.tagName("h2")).getText().contains("Projetos");
 }

}

Observe que os códigos desses Page Objects encapsulam toda a lógica de acesso aos elementos de sua página – todo o código que utiliza a API do WebDriver está encapsulado em seus métodos. Veja também que os métodos expostos  por eles são métodos que utilizam uma linguagem natural (visita(), autentica(), isValida()) e não fazem referência a elementos HTML. Como vamos ver na listagem a seguir, isso deixa o código de teste muito mais legível.

public class LoginSistemaComPageObjectTest {

 private WebDriver driver;

 @Before
 public void before() {
   driver = new ChromeDriver();
 }

 @Test
 public void loginComSucesso() {
   LoginPage loginPage = new LoginPage(driver);
   ListaProjetosPage homePage = loginPage.
                                visita("http://localhost:8080/simpletests").
                                autentica("joao@mpf.mp.br","123456");

   assertTrue(homePage.isValida());
 }

 @After
 public void after() {
   driver.quit();
 }

}

Observe o código em destaque. Este é o código que efetivamente realiza o nossos testes. Em vez de chamar métodos da API do WebDriver ele chama métodos dos nossos Page Objects – o que é muito mais natural. Fica bem mais fácil, também, entender o que ele está fazendo: visita a página de login; faz a autenticação; e verifica se autenticação foi feita com sucesso checando se estamos na página inicial da aplicação (que foi retornada pelo método de autenticação do LoginPage). E toda a lógica de acesso a elementos das páginas HTML está encapsulada no Page Object – se a página mudar, os nossos testes não precisam ser modificados – os pontos de mudança estão isolados.

Compare este código de teste com o código anterior de LoginSistemaSemPageObjectTest, que não faz uso de Page Objects, e tire suas conclusões.

Para finalizar, vamos criar mais um caso de teste em nossa aplicação e demonstrar como o Page Object facilita o reuso de código de testes.

Reutilizando Page Objects

Vamos agora criar o caso de teste que faz a inclusão de um novo projeto em nosso sistema. Esse caso de teste vai realizar os seguintes passos: realizar a autenticação no sistema; clicar no botão “Novo Projeto”; preencher o formulário com os dados do novo projeto e clicar em Salvar; e, finalmente, verificar que o projeto foi incluído com sucesso.

Na parte de cima o formulário de cadastro de projetos. Na parte de baixo a listagem de projetos com a mensagem de sucesso após incluir um projeto.
Na parte de cima o formulário de cadastro de projetos. Na parte de baixo a listagem de projetos com a mensagem de sucesso após incluir um projeto.

Fica claro, então, que esse caso de teste vai precisar ter o código que faz a autenticação no sistema. Sem o uso de Page Object, teríamos que replicar o código de login abaixo no nosso caso de teste.

	// Acessa a página inicial que nos redireciona para a página de login
	driver.get("http://localhost:8080/simpletests");

	// Informa o usuário/senha e clica no botão de login
	driver.findElement(By.name("username")).sendKeys("joao@mpf.mp.br");
	driver.findElement(By.name("password")).sendKeys("123456");
	driver.findElement(By.cssSelector("input[type='submit'")).click();	

Duplicação de código sempre é ruim. Se a página de login mudar, vamos ter que modificar esse código em todos os pontos que aparece – e vai aparecer em muitos pontos, pois a autenticação é um pré-requisito para acessar a nossa aplicação. É aí que aparece mais uma vantagem do uso do Page Object. No nosso novo caso de teste, vamos reutilizar o LoginPage. Os códigos estão abaixo.

Primeiro o código modificado de ListaProjetosPage (as mudanças estão em destaque).

public class ListaProjetosPage {

    private WebDriver driver;

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

    public InclusaoProjetoPage novoProjeto() {
      driver.findElement(By.linkText("Novo Projeto")).click();
      return new InclusaoProjetoPage(driver);
    }

    public boolean projetoIncluidoSucesso() {
      WebElement divSucesso = driver.findElement(By.className("alert-success"));
      return divSucesso.getText().contains("Projeto incluído com sucesso!");
    }

    public boolean isValida() {
      return temBarraNavegacao() && temListagemProjetos();
    }

    private boolean temBarraNavegacao() {
      return driver.findElement(By.className("navbar")) != null;
    }

    private boolean temListagemProjetos() {
      return driver.findElement(By.tagName("h2")).getText().contains("Projetos");
    }

}

No código anterior foram adicionados métodos que permite o acesso a página de inclusão de projeto (novoProjeto()) e que verifica se o processo foi incluído com sucesso (projetoIncluidoSucesso()). Abaixo o código do Page Object que representa a inclusão de um novo projeto.

public class InclusaoProjetoPage {

    private WebDriver driver;

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

    public InclusaoProjetoPage nome(String nomeProjeto) {
      driver.findElement(By.name("nome")).sendKeys(nomeProjeto);
      return this;
    }

    public InclusaoProjetoPage descricao(String descricaoProjeto) {
      driver.findElement(By.name("descricao")).sendKeys(descricaoProjeto);
      return this;
    }

    public ListaProjetosPage salvarProjeto() {
      driver.findElement(By.cssSelector("button.btn.btn-primary")).click();
      return new ListaProjetosPage(driver);
    }

}

Esse código fornece métodos para preenchimento de dados do projeto e para salvar o projeto (clicando no botão “Salvar “).

E por fim, o código que testa a inclusão do projeto com sucesso.

public class InclusaoNovoProjetoTest {

  private WebDriver driver;

  @Before
  public void before() {
    driver = new ChromeDriver();
    driver.manage().timeouts().implicitlyWait(30, TimeUnit.SECONDS);
  }

  @Test
  public void inclusaoProjetoComSucesso() {
    LoginPage loginPage = new LoginPage(driver);
    ListaProjetosPage homePage = loginPage.
			visita("http://localhost:8080/simpletests").
			autentica("joao@mpf.mp.br","123456");

    InclusaoProjetoPage inclusaoProjetoPage = homePage.novoProjeto();

    ListaProjetosPage listaProjetosPage = inclusaoProjetoPage.
        nome("Projeto novo de Teste").descricao("Essa é a descrição do projeto.").
        salvarProjeto();

    assertTrue(listaProjetosPage.projetoIncluidoSucesso());
  }

  @After
  public void after() {
     driver.quit();
  }

}

Veja o código de teste em destaque. Ele reutiliza os Page Objects de login (LoginPage) e de listagem de projetos (ListaProjetosPage). Mais uma vez, quero destacar a clareza de intenção desse código: ele faz chamada aos Page Objects para a realização de ações e por fim faz a asserção para verificar se houve sucesso na inclusão do projeto – e o código de manipulação das páginas está encapsulado e abstraído do código de teste.

Conclusão

A ideia deste post foi mostrar como utilizar o padrão Page Object para tornar o seu código de teste de aceitação melhor por meio de práticas como separação de responsabilidades, abstração e encapsulamento. Todo e qualquer código existente em nossa aplicação deve ter um tratamento especial, pois, invariavelmente, grandes são as chances de ele precisar ser evoluído e/ou sofrer manutenção. E essa mentalidade, é claro, também deve se aplicar aos nossos códigos de teste.

Como complemento a este post sobre boas práticas no código de teste, sugiro a leitura de 2 excelentes artigos do Stefan Teixeira sobre o uso de design patterns para melhorar o código de testes. O primeiro post fala sobre o padrão Builder Fluent Interfaces. Já o segundo fala sobre o StepBuilder. Leituras obrigatórias!

Relembrando que todo o código apresentando neste post pode ser acessado aqui em meu GitHub.

  • Bruno Taboada

    Não conhecia esse padrão, interessante. Vale ressaltar que, mesmo que já mencionado no artigo, as técnicas utilizadas (Encapsulamento e Abstração) em conjunto com o princípio de Separação de responsabilidades é que no fundo permitiu solucionar esse problema de uma forma inteligente, Viva a OO.

    No entanto, apesar de deixar o código de teste bem legível, acho que o código de teste fica bem orientado a página (LoginPage, ResetPasswordPage e etc), e quando a aplicação é orientada a serviço? poderiamos usar a mesmas técnicas apresentadas e criar um pattern Service Object ou algo do tipo?

    Bom artigo. Parabéns meu caro. Sucesso!

    • Bruno, esse padrão é mesmo útil. Tenho utilizados em meus testes de aceitação as técnicas que ele nos estimula a empregar deixam claro seu benefício.

      Ele relamente é um padrão voltado para a abstração de interfaces visuais; não apenas páginas HTML, mas quaisquer tipo de elemento visual (até mesmo aplicações feitas em Swing, por exemplo). O Martin Fowler fala mais sobre isso aqui: http://martinfowler.com/bliki/PageObject.html.

      Penso que seria possível sim aplicar a mesma ideia para testes em aplicações orientadas a serviços (SOA ou REST). Abstrair as chamadas ao serviço em um Service Object, como você disse, acredito que traria o mesmo benefício. Vou pensar mais sobre isso. ;-D

      Valeu pela audiência! Abraços!

  • Ótimo post André, parabéns!
    A única alteração/dica que eu faria é externalizar o texto que será validado, onde usaríamos o assertEquals ao invés do asserTrue no teu Page Objects ListaProjetosPage.

    Vai ficar mais legível para quem der manutenção e for verificar a diferença de validação de textos com o equals, uma vez que ele vai mostrar essa diferença (aplicar um diff), além de expor isso no código de teste usando o Page Objects.

    Grande abraço!

    • Ótima dica, Elias!

      Realmente fica mais legível e claro desse jeito que você sugeriu. Vou levar em consideração nos meus próximos códigos.

      Abraços!

  • Renan Elias

    Muito bom o Post.
    Recomendo.

    • Valeu @renan_elias:disqus! Se puder compartilhar, agradeço!

      • Renan Elias

        Com certeza, já compartilhei

  • Renato Nunes

    Excelente artigo André. Conseguiu exporte de forma clara e objetiva o conceito e a pratica do Page Objects. Estamos começando a implementar os testes com selenium na empresa em que trabalho e com certeza esse artigo irá ajudar bastante. Muito obrigado.

    • Fico muito feliz em poder ajudar, @disqus_lqL43fWFx8:disqus! Qualquer coisa, estamos aí para discutir mais.
      Abraços!