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 🙂

Mapa brasileiro de qualidade do ar

 

Desde a versão 2.0 do aplicativo Post Fumaça Preta, disponibilizada para iOS, passamos a informar a qualidade do ar a partir da localização do usuário. Além de ter acesso às informações oriundas da estação de monitoramento mais próxima, o usuário passou a poder visualizar o mapa que apresenta toda a rede de monitoramento. Cada estação posicionada no mapa informando: a data e a hora de publicação dos dados, a classificação e o índice da qualidade do ar, o poluente determinante no cálculo do índice, e ainda a evolução do IQA através de um gráfico histórico.

Para que isso fosse possível, muitas linhas de código e uma técnica particular, chamada web scraping. Com ela, foi possível extrair de forma automatizada e constante as informações de qualidade do ar publicadas nos sites dos diversos órgãos ambientais brasileiros que possuem rede de monitoramento (para realizar essa extração, utilizamos o Jsoup), e centralizá-las numa base de dados estruturada. Criamos então algo ainda inexistente no momento, o mapa brasileiro de qualidade do ar!

Como nem tudo são flores, algumas dificuldades são encontradas para manter o mapa atualizado, como aumentar a abrangência à medida que mais órgãos passam a publicar os dados, se adaptar a novos formatos adotados pelos órgãos nos seus sites, e conhecer a localização das estações de monitoramento. E apesar dos órgãos se basearem nos padrões definidos pelo Ministério do Meio Ambiente, podem possuir tabelas de classificação e nomenclaturas diferentes, que o mapa precisa seguir à risca.

Hoje são 7 os órgãos ambientais cujos dados publicados de qualidade do ar são obtidos: CETESB (Companhia Ambiental do Estado de São Paulo), FEAM (Fundação Estadual do Meio Ambiente – MG), IAP (Instituto Ambiental do Paraná), IEMA (Instituto Estadual de Meio Ambiente e Recursos Hídricos – ES), INEA (Instituto Estadual do Ambiente – RJ), INEMA (Instituto do Meio Ambiente e Recursos Hídricos – BA) e SMAC (Secretaria Municipal de Meio Ambiente – Rio de Janeiro). Nos avise se souberem de mais órgãos!

O módulo de qualidade do ar faz parte atualmente do Post Denúncia, mas dada sua relevância, queremos separá-lo. É nosso desejo usá-lo como base para a construção de uma plataforma aberta voltada especificamente para a qualidade do ar. O Post Fumaça Preta passaria então a ser cliente dessa nova plataforma, que disponibilizaria uma API para uso não somente por aplicativos, mas por sites e pessoas em geral. Esperamos que curtam a ideia e se engajem no projeto!

Google Maps Geocoding API

 

A plataforma do Post Denúncia usa a Google Maps Geocoding API para identificar a localização do denunciante. Quando as coordenadas (latitude e longitude) são recebidas, são submetidas à API. Ela então retorna várias informações, dentre elas o endereço, o bairro, a cidade e o estado onde o problema urbano foi reportado. Desta forma, a denúncia pode seguir para o órgão com toda a informação necessária para seu tratamento.

Para se comunicar com a API, foi desenvolvido um módulo cliente, apesar da própria equipe do Google Maps disponibilizar no GitHub uma implementação cliente em Java, aqui. Não tínhamos necessidade de algo complexo, enquanto a solução oficial contemplava não apenas a Geocoding API, mas outras APIs do Google Maps também. Construímos então uma biblioteca Java simples, que no momento deste post está na versão 1.2, e que pode ser obtida no GitHub da mesma forma, aqui.

No projeto google-geocode são duas as principais classes: GoogleGeocode e GeocodeResponse. A classe GoogleGeocode faz a comunicação com a Google Maps Geocoding API propriamente dita, passando como parâmetro as coordenadas geográficas ou um endereço específico, e convertendo a resposta em formato JSON para um objeto da classe GeocodeResponse (para realizar essa conversão, utilizamos o Jackson). Com a classe GeocodeResponse é possível conhecer o status da resposta e percorrer a lista com os resultados fornecidos pela API.

Como o Post Denúncia se restringe ao território brasileiro, as classes foram estendidas em BrazilGoogleGeocode e BrazilGeocodeResponse. Elas podem ser visualizadas aqui. Deste modo, pôde-se definir PT_br como idioma para o retorno dos resultados, e também implementar métodos específicos que retornassem adequadamente o nome do bairro e da cidade.

Tenha em mente que, ao usar o serviço de geolocalização do Google, você está criando uma dependência para sua aplicação, e precisa gerenciar isso. A Google garante disponibilidade para quem estiver em conformidade com os Termos de Serviço, que requer dentre outras coisas que sua aplicação seja pública e gratuita.

A Google define também limites de utilização. Os usuários da API padrão, não paga (nosso caso), podem fazer até 2.500 requisições por dia, e no máximo 10 solicitações por segundo. Isso é controlado através da chave da aplicação, que é passada como parâmetro na requisição. Apesar de ser uma recomendação da Google, na solução que demos não passamos a chave. O controle, nesse caso, é feito a partir do IP de onde as requisições estão sendo feitas. Se você for usar a nossa biblioteca google-geocode, tenha consciência disso. Caso queira colaborar com o projeto e implementar esta melhoria, basta fazer um pull request no repositório do Github.

Se não desejar o código do google-geocode, mas quiser usá-lo no seu projeto Maven, basta adicionar no pom.xml:

    <repositories>
        <repository>
            <id>esign-repo</id>
            <name>Esign Maven Repository</name>
            <url>http://maven.esign.com.br</url>
        </repository>
    </repositories>

É necessário informar o repositório da Esign, para que o Maven saiba de onde baixar a dependência:

    <dependencies>
        <dependency>
            <groupId>br.com.esign</groupId>
            <artifactId>google-geocode</artifactId>
            <version>1.2.0</version>
        </dependency>
    </dependencies>

Fique à vontade para usar! 🙂

3 anos de Post Denúncia

Há 3 anos nasceu o Post Denúncia, serviço da Esign para cidadãos denunciarem problemas urbanos aos órgãos competentes, através de aplicativos para smartphones. Com o propósito de promover a cidadania e a qualidade de vida, foi criada uma plataforma capaz de receber os problemas reportados e encaminhá-los aos órgãos públicos responsáveis por tratá-los. Nosso objetivo foi empoderar as pessoas, disponibilizando uma ferramenta que as tornassem agentes da mudança, e ajudar o poder público, indicando onde se faz necessária sua atuação.

O projeto então lançou o aplicativo Post Fumaça Preta, que permite a denúncia de veículos que poluem o ar, emitindo fumaça irregularmente. Ele está disponível na Apple Store e no Google Play. A partir da versão 2.0, passou a informar a qualidade do ar obtida da rede de monitoramento mantida por órgãos públicos ligados ao meio ambiente. No estado de São Paulo, as denúncias feitas pelo aplicativo são enviadas para a CETESB, que é o órgão responsável também pela divulgação das informações de qualidade do ar obtidas das suas diversas estações de monitoramento.

Embora tenhamos orgulho do projeto, e vários motivos de satisfação como a indicação para o Prêmio Cidadão Sustentável, encontramos muitas dificuldades em mantê-lo. Apesar dos esforços, não conseguimos torná-lo rentável, e não foi possível lançar os demais aplicativos para denunciar outros tipos de problemas urbanos. De toda forma, não desistimos do nosso sonho, e por isso estamos abrindo o código fonte da plataforma, na esperança de mais desenvolvedores colaborarem. O código está disponível no Github, e contamos com você que acredita que o Post Denúncia pode fazer a diferença 🙂

Post Denúncia nas redes sociais:       

Editado em 24/02/2017: Infelizmente o Post Denúncia foi encerrado. Mais informações em Fim do Post Denúncia.