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 🙂

Qualidade da sua API REST com REST-assured

rest-assured

Este é o primeiro post de uma série que visa ajudar os desenvolvedores a testarem efetivamente o código que produzem e, assim, fomentar a cultura da qualidade de software. As ferramentas que serão apresentadas testam uma aplicação exemplo de logística, chamada logistics, cujo código está disponível no nosso Github.

A aplicação permite criar mapas e rotas dentro destes mapas. O objetivo é encontrar o melhor caminho, entre o ponto de origem e o de destino, dada a autonomia do veículo e o preço do combustível. O melhor caminho é o mais barato. Uma API REST foi desenvolvida para expor as funcionalidades da aplicação, e vamos agora mostrar como testá-la com o REST-assured.

    <dependencies>
        <dependency>
            <groupId>com.jayway.restassured</groupId>
            <artifactId>rest-assured</artifactId>
            <version>2.5.0</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

Para testar, é preciso que a aplicação esteja disponível num servidor Java EE (particularmente, usamos o Wildfly). Por padrão, o REST-assured assume que a aplicação está disponível na porta 8080 do localhost, mas isso pode ser alterado definindo-se os campos baseURI e/ou port. No nosso teste, não precisamos alterar, então basta informar à ferramenta o endpoint:

    @Before
    public void setup() {
        RestAssured.basePath = "/logistics/api/maps";
    }

Este código faz parte do projeto logistics-test-restassured, com duas classes JUnit que utilizam o REST-assured: RESTTestRESTResponseSchemaTest. Ambas testam as funcionalidades da aplicação logistics na sequencia determinada pelos nomes dos métodos, comportamento definido pela annotation FixMethodOrder com o parâmetro MethodSorters.NAME_ASCENDING. A ordem dos testes é a seguinte:

  1. Criação de um mapa
  2. Verificação da existência do mapa recém criado
  3. Verificação da constraint de nome único para mapas
  4. Criação de rotas para o mapa
  5. Verificação da constraint de nome único para rotas
  6. Exclusão de uma rota
  7. Obtenção da melhor rota
  8. Exclusão do mapa

O REST-assured se baseia no modelo given-when-then para a realização dos testes. Segundo esse modelo, partindo de um cenário (given) e um comportamento (when) subsequente, teremos o resultado esperado (then). A diferença entre as duas classes é basicamente como avaliam o resultado esperado. Primeiro, vejamos como o teste da criação de um mapa é feito na RESTTest:

    @Test
    public void testA() {
        mapSlug = RestAssured
            .given()
                .contentType(ContentType.JSON)
                .body("{\"name\": \"REST-assured Test\"}")
            .when()
                .post()
            .then()
                .statusCode(200)
                .contentType(ContentType.JSON)
                .body("code", equalTo(200))
                .body("status", equalTo("success"))
                .body("data", notNullValue())
                .body("data.slug", notNullValue())
            .extract()
                .path("data.slug");
    }

Neste método, o primeiro da sequencia, é testada a criação de um mapa com o nome “REST-assured Test”, submetido no formato JSON via POST. É esperada uma resposta 200 do HTTP (sucesso) e o resultado retornado, também no formato JSON, representa o mapa recém criado no campo data, obrigatoriamente com o slug que o identifica definido. O slug é então extraído para uso nos testes posteriores (requer dependência abaixo).

        <dependency>
            <groupId>com.jayway.restassured</groupId>
            <artifactId>json-path</artifactId>
            <version>2.5.0</version>
            <scope>test</scope>
        </dependency>

Vejamos agora como o teste da criação de um mapa é feito na RESTResponseSchemaTest:

    @Test
    public void testA() {
        RestAssured
            .given()
                .contentType(ContentType.JSON)
                .body("{\"name\": \"REST-assured JSON Schema Test\"}")
            .when()
                .post()
            .then()
                .statusCode(200)
                .contentType(ContentType.JSON)
                .body(matchesJsonSchemaInClasspath("map-response-schema.json"));
    }

Observe que parte das mesmas condições, mas o resultado é avaliado de modo distinto. Como o resultado esperado está no formato JSON, é possível constrastá-lo com seu JSON schema, que nada mais é do que a descrição do JSON definido como resposta. Podemos dizer que o JSON schema está para o JSON assim como o XSD está para o XML. O REST-assured valida então se o JSON schema contido em map-response-schema.json bate com o retorno da API (requer dependência abaixo).

        <dependency>
            <groupId>com.jayway.restassured</groupId>
            <artifactId>json-schema-validator</artifactId>
            <version>2.5.0</version>
            <scope>test</scope>
        </dependency>

Bem, estes exemplos tentam mostrar como o REST-assured é uma excelente ferramenta para testes de APIs REST. Uma vez definidos os endpoints, os métodos, as entradas e saídas, é possível utilizá-la para garantir a qualidade da API. Os testes podem ser executados por linha de comando (mvn test) ou podem fazer parte de um pipeline de entrega contínua.

Espero que tenha gostado, e aguarde o próximo post 🙂