Como sempre entregar seu projeto open source com qualidade

Você tem então seu projeto open source no Github e quer deixá-lo disponível para uso enquanto ele evolui. Isso é muito importante porque o quanto antes ele é utilizado, novas funcionalidades são testadas, bugs são encontrados, mais rápido o software melhora a cada dia. Os usuários podem ter novidades, que agreguem valor, constantemente, e os colaboradores do projeto podem ter o feedback necessário na mesma velocidade. Você pode até não saber o nome, mas o que você quer é implementar um processo de integração e entrega contínuas no ciclo de desenvolvimento do seu software, prática de quem adota DevOps.

O Github se integra a vários serviços distintos de integração contínua. Eles podem ser vistos em http://bit.ly/2yx6k9h. No exemplo dado aqui hoje vamos optar pelo Travis CI, gratuito para projetos open source. O projeto é o Logistics, uma aplicação Java EE (agora EE4J) que roda no Wildfly e usa um banco de dados MongoDB.

O Travis CI requer um arquivo no repositório, chamado .travis.yml. Um exemplo básico dele e mais informações para começar a usar o serviço podem ser visualizados em Getting started. Vamos conferir agora o que o .travis.yml do projeto Logistics instrui o Travis CI a fazer, sempre que um push é feito. Detalhes de como personalizar o processo pode ser conferido em Customizing the Build.

language: java
sudo: required
install: true

services:
  - docker

addons:
  sonarcloud:
    organization: "gustavomcarmo-github"

jdk:
  - oraclejdk8

script:
  - mvn clean org.jacoco:jacoco-maven-plugin:prepare-agent package sonar:sonar
  - docker build --tag=esignbr/logistics .

cache:
  directories:
    - '$HOME/.m2/repository'
    - '$HOME/.sonar/cache'

after_success:
  - "curl -T logistics-ear/target/logistics-ear-1.0-SNAPSHOT.ear -u $FTP_USER:$FTP_PASSWORD ftp://esign.com.br/appservers/wildfly-10.0.0.Final/standalone/deployments/"
  - docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD"
  - docker push esignbr/logistics

Definindo a linguagem de programação

É o primeiro passo, logo na linha inicial. No caso do projeto Logistics, é Java, mas poderia ser Python ou NodeJS. Nas linhas 12 e 13, é possível ainda especificar qual JDK será utilizada. Para compilar a aplicação, é necessária a versão 8.

Gerando os artefatos Java

Para implantar a aplicação Logistics no Wildfly, geramos o arquivo EAR, contendo o JAR do módulo backend do projeto (logistics-ejb) e o WAR do módulo frontend (logistics-web). Para isso, usamos o Maven, responsável essencialmente pela compilação, pela execução dos testes unitários e pelo empacotamento (package) necessário para a entrega. O Maven permite também a utilização de plugins, que estendem sua funcionalidade. Na linha 16 instruímos o Travis CI a executar o Maven (mvn) passando como parâmetro as tarefas a serem desempenhadas por ele.

Avaliando a qualidade do código

O Travis CI tem integração com o SonarCloud, instância do SonarQube na nuvem, disponível para avaliar projetos open source. O SonarQube é uma excelente ferramenta para analisar continuamente a qualidade do código Java e também de outras linguagens de programação. Ela tem uma funcionalidade chamada Quality Gate, que permite que critérios específicos de qualidade sejam definidos para considerar a aplicação apta para entrega. O Quality Gate usado pelo projeto Logistics é o default da ferramenta.

Para integrar ao Travis CI, primeiro é preciso se registrar no SonarCloud e criar um token de autenticação lá. Nas linhas 8 a 10 é configurado o addon do SonarCloud. Observe que é informada a organização que o identifica no serviço, mas não o token. O token é informado no Travis CI como variável de ambiente (SONAR_TOKEN), para não ficar explícito no arquivo .travis.yml.

Uma vez plugado o SonarCloud, basta instruir o Travis CI a rodar a análise do código fonte. Como estamos usando o Maven, e o plugin do SonarQube para o Maven, basta passar como parâmetro para o comando mvn a tarefa sonar:sonar.

Além disso, é preciso analisar a cobertura dos testes unitários, pois é critério de qualidade definido no Quality Gate padrão do SonarQube. Neste caso, usamos o JaCoCo, e seu plugin para o Maven, que executamos anteriormente à análise do SonarQube, ao passar org.jacoco:jacoco-maven-plugin:prepare-agent para o comando mvn.

Mais sobre a integração do SonarCloud ao Travis CI pode ser visto em Using SonarCloud with Travis CI. Os resultados das análises do código fonte da aplicação Logistics realizadas pelo SonarQube podem ser visualizados em https://sonarcloud.io/dashboard?id=br.com.esign%3Alogistics.

Gerando a imagem Docker

Uma alternativa à geração do EAR, que precisa ser implantado numa instância do Wildfly específica, é gerar a imagem Docker da aplicação com o Wildfly e a própria aplicação já embutidos. Neste caso, a aplicação e tudo que ela depende pra ser executada é empacotado junto numa imagem. Para rodar a aplicação, basta então rodar a imagem numa máquina que tiver o Docker instalado.

Para construir a imagem da aplicação Logistics, usamos como base a imagem jboss/wildfly, aplicamos a configuração necessária para acesso ao MongoDB, e implantamos o EAR gerado, conforme definido no Dockerfile. A construção é realizada na linha 17 do arquivo .travis.yml, a partir do comando docker build. A execução de comandos Docker é possível com a utilização do serviço informada nas linhas 5 e 6 e definindo o sudo como required, como aparece na linha 2. Mais informações de como usar o Docker no Travis CI podem ser visualizadas em Using Docker in Builds.

Entregando efetivamente o software

Após analisada a qualidade do software e gerados os artefatos necessários, a entrega pode efetivamente ocorrer. No arquivo .travis.yml, isso é definido em after_success (linha 24).

Primeiramente, o EAR é colocado via FTP no diretório de deployments do Wildfly que fica no nosso domínio (esign.com.br), como pode ser observado na linha 25. As credenciais para acesso ao nosso servidor são por questões de segurança obtidos de variáveis de ambiente. Com este deploy automático, estamos sempre disponibilizando a versão mais nova da aplicação em http://www.esign.com.br/logistics.

Por último, subimos para o Docker Hub a imagem Docker, como pode ser verificado nas linhas 26 e 27. As credenciais para login no Docker Hub são por questões de segurança também obtidos de variáveis de ambiente (linha 26). Com o push automático (linha 27), a última versão da imagem, com a última versão da aplicação, estará sempre disponível para download e execução em máquinas com Docker. O repositório da imagem no Docker Hub é https://hub.docker.com/r/esignbr/logistics.

Otimizando o tempo de build

O Travis CI permite ainda algumas configurações que fazem diminuir o tempo de build. Na linha 3 do .travis.yml, por exemplo, é desabilitada a etapa de resolução das dependências, quando normalmente muitos downloads acontecem. Além disso, com a definição do uso de cache (linhas 19 a 22), o Travis CI sabe onde buscar as dependências outrora baixadas, eliminando assim o download desnecessário.

À medida que o software cresce, é normal que o tempo de build aumente. Não são apenas mais linhas de código, mas novos testes unitários são realizados. De toda forma, é preciso acompanhar e controlar o tempo de build, para manter todo o processo saudável. Não se esqueça que o objetivo é ter feedbacks rápidos. Caso aumente demais, considere dividir o projeto. No caso do Logistics, os builds podem ser vistos em https://travis-ci.org/esign-consulting/logistics.

Conclusão

Esperamos que com este post você consiga implantar um pipeline de entrega do seu projeto open source. Mais do que isso, que você possa evoluí-lo e compartilhá-lo com a comunidade e conosco também. Nunca se esqueça que compartilhando nossas soluções é que mantemos sempre forte o movimento open source.

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 🙂