Qualidade da sua UI com Selenium

selenium

Este é o segundo post da série que tem como objetivo disseminar a cultura da qualidade de software, apresentando ferramentas que permitam os desenvolvedores incorporarem no seu dia-a-dia a prática do teste do código que produzem. Desta vez testaremos a interface do usuário (UI) da aplicação logistics, a mesma aplicação web que usamos como exemplo no post anterior, e cujo código está aqui.

O frontend da aplicação logistics foi construído com o AngularJS, framework JavaScript da Google que implementa o padrão de arquitetura MVC, e que tem uma característica bem peculiar motivo principal da sua fama: o two-way data binding. O AngularJS foi combinado ao Bootstrap, conhecido framework frontend com origem no Twitter, na criação da página única da aplicação (single-page application).

Vamos testar as funcionalidades da página web de forma automatizada com o Selenium. Com esta ferramenta, é possível simular uma pessoa usando literalmente a aplicação, ou seja, digitando dados num campo, clicando num botão, etc. Só que isso de modo previamente programado, bastando escolher dentre os vários web browsers que ela suporta onde será feita a simulação. No nosso caso, o Mozilla Firefox.

Para reproduzir o comportamento do usuário e testar nossa UI, usamos um design pattern do Selenium chamado Page Object Model, onde as telas são representadas por classes Java, e os elementos das telas são seus atributos (a escolha desta solução teve inspiração neste excelente post no blog da Toptal). Apesar da nossa aplicação ser SPA, fizemos uso de telas do tipo modal, abertas a partir da tela principal, e cada foi representada:

  • HomePage.java (tela principal)
  • AddMapModal.java (tela modal para adicionar novo mapa)
  • AddRouteModal.java (tela modal para adicionar rotas a um mapa)
  • BestRouteModal.java (tela modal para descobrir melhor rota)
  • RemoveMapModal.java (tela modal para confirmar a execução de mapa)

Bem, vamos mostrar um pouco de código. Todo o código deste exemplo está aqui. Fique à vontade para clonar o repositório GIT e contribuir com o projeto. Primeiro, seguem as dependências referentes ao Selenium, para serem adicionadas ao pom.xml:

        <dependency>
            <groupId>org.seleniumhq.selenium</groupId>
            <artifactId>selenium-firefox-driver</artifactId>
            <version>2.53.0</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.seleniumhq.selenium</groupId>
            <artifactId>selenium-support</artifactId>
            <version>2.53.0</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.seleniumhq.selenium</groupId>
            <artifactId>selenium-java</artifactId>
            <version>2.53.0</version>
            <scope>test</scope>
        </dependency>

Na página principal da aplicação, veja por exemplo o trecho referente à tela modal para adicionar rotas a um mapa existente:

  <script type="text/ng-template" id="addRouteModal.html">
    <div class="modal-header">
      <h3 class="modal-title">{{map.name}}: New Route</h3>
    </div>
    <div class="modal-body">
      <div class="row">
        <div class="col-md-4">
          <label for="originName">Origin:</label>
          <input type="text" id="originName" ng-model="route.origin.name" class="form-control" autofocus>
        </div>
        <div class="col-md-4">
          <label for="destinationName">Destination:</label>
          <input type="text" id="destinationName" ng-model="route.destination.name" class="form-control">
        </div>
        <div class="col-md-4">
          <label for="distance">Distance (Km):</label>
          <input type="text" id="distance" ng-model="route.distance" class="form-control">
        </div>
      </div>
      <div class="row" style="margin-top: 20px;">
        <div class="col-md-12">
          <alert>The opposite route ({{route.destination.name}} -> {{route.origin.name}}) will be also created.</alert>
        </div>
      </div>
    </div>
    <div class="modal-footer">
      <button name="addRouteOkButton" class="btn btn-primary" ng-click="ok()">OK</button>
      <button name="addRouteCancelButton" class="btn btn-warning" ng-click="cancel()">Cancel</button>
    </div>
  </script>

Esta é a tela:

newroute

E este é o código de AddRouteModal.java, classe Java que representa a tela em questão:

public class AddRouteModal {

    private final WebDriver driver;

    @FindBy(tagName = "h3")
    private WebElement heading;

    @FindBy(id = "originName")
    private WebElement originName;
    
    @FindBy(id = "destinationName")
    private WebElement destinationName;
    
    @FindBy(id = "distance")
    private WebElement distance;

    @FindBy(name = "addRouteOkButton")
    private WebElement addRouteOkButton;

    @FindBy(name = "addRouteCancelButton")
    private WebElement addRouteCancelButton;

    public AddRouteModal(WebDriver driver) {
        this.driver = driver;
        PageFactory.initElements(driver, this);
    }

    public boolean isPageOpened(String map) {
        return heading.getText().contains(map.concat(": New Route"));
    }
    
    public void setOriginName(String origin) {
        originName.clear();
        originName.sendKeys(origin);
    }
    
    public void setDestinationName(String destination) {
        destinationName.clear();
        destinationName.sendKeys(destination);
    }
    
    public void setDistance(String d) {
        distance.clear();
        distance.sendKeys(d);
    }
    
    public void clickOnAddRouteOkButton() {
        addRouteOkButton.click();
    }
    
    public void clickOnAddRouteCancelButton() {
        addRouteCancelButton.click();
    }

}

Observe que, na construção da classe, os atributos do tipo WebElement são inicializados pelo PageFactory. Cada atributo deste tipo representa um elemento de interface, que para ser identificado usa-se o annotation FindBy. Especificamente nesta classe, os elementos foram encontrados a partir do tagName, id e name, mas isto pode ser feito por maneiras distintas, inclusive utilizando XPath.

Feita a inicialização dos atributos, seus métodos podem ser invocados e os elementos de interface é que reagem. Durante a execução do teste, a execução de addRouteCancelButton.click(), por exemplo, faria com que a tela modal fosse fechada. Do mesmo modo, métodos como clear e sendKeys de WebElements que representam campos de formulário, respectivamente refletem a limpeza e a definição de seu conteúdo.

Uma vez construídas as classes que reproduzem o comportamento das telas, foi possível desenvolver o teste propriamente dito. A classe UITest realiza os testes na sequência (annotation FixMethodOrder do JUnit), passando por todas as funcionalidades de telas, como se fosse numa interação entre uma pessoa e a aplicação rodando no browser. O trecho de código abaixo, por exemplo, testa a criação de rota, usando a classe AddRouteModal, apresentada anteriormente:

        home.clickOnAddRouteButton();
        AddRouteModal modal = new AddRouteModal(driver);
        assertTrue(modal.isPageOpened(MAP));
        
        modal.setOriginName(PLACEA);
        modal.setDestinationName(PLACEB);
        modal.setDistance("10");
        modal.clickOnAddRouteOkButton();
        assertTrue(home.isAlertAddRouteSuccessMessage(PLACEA, PLACEB));

Enfim, esperamos ter contribuído um pouco mais para disseminar o conhecimento de ferramentas de teste. O Selenium é um ótima ferramenta para garantir o bom funcionamento da sua interface com o usuário e, melhor, de maneira automatizada. O teste pode ser adicionado ao processo de entrega contínua ou simplesmente acionado de forma ad hoc.

Críticas e sugestões, sintam-se à vontade 🙂