The Social App: Integrando dados de Cobertura de Código

Padrão

Se você ainda não conhece as motivações deste projeto, leia o primeiro artigo aqui.

Olá pessoal! No último post realizamos uma configuração inicial do tema do nosso aplicativo de forma a diferenciar as builds de debug e release. No post de hoje, vamos configurar uma métrica importante para o desenvolvimento de qualquer projeto de software, a cobertura de código.

Simplificando ao máximo o conceito (já que a explicação dele foge um pouco ao escopo deste post), a métrica diz respeito a quanto do seu código é “exercitado” durante a execução dos testes. Os relatórios de cobertura geralmente fornecem formas de mensurar não só percentualmente quanto do código foi coberto pelos testes, mas também pode dar insights sobre quais partes do código poderiam receber mais testes.

O primeiro passo é habilitarmos a geração desse dado no projeto. Porém, neste momento não é possível gerar este dado, já que nosso projeto não possui nenhum teste. Na verdade, ele praticamente não possui código, somente uma Activity vazia que ainda não faz nada. Poderíamos criar um teste simples de Espresso, para validar a inicialização da Activity, porém ainda não resolvemos o problema da execução de testes instrumentados no nosso CI (inclusive, se você quer ler mais sobre a diferença entre testes instrumentados e locais, tem um post excelente do Victor Nascimento no Medium). Dado esse cenário, vou utilizar o Robolectric para criar um teste que, apesar de utilizar classes do Android, roda localmente na JVM.

Para integrarmos o Robolectric em nosso projeto, é necessário adicionar a sua dependência, dentro do escopo de testes (testImplementation). Além disso, como o Robolectric utiliza resources do Android, precisamos habilitar a disponibilização dos resources para os testes unitários. Caso contrário, nossos testes falharão por não encontrar qualquer referência a resources adicionados por nós no projeto (como layouts, strings, drawables e outros).

<br />
android {<br />
  ...<br />
  testOptions {<br />
    unitTests.includeAndroidResources true<br />
  }<br />
}</p>
<p>dependencies {<br />
  ...<br />
  testImplementation 'org.robolectric:robolectric:3.8'<br />
}<br />

Dentro da pasta src/test/kotlin, vamos criar uma classe de teste bem simples, chamada MainActivityTest, que simplesmente fará uma validação se a Activity foi criada com sucesso pelo Robolectric. Apesar de ser um teste sem muito valor para o app (afinal, estamos testando se o Robolectric funciona), esse teste gerará alguma cobertura para o nosso projeto.

package net.rafaeltoledo.social</p>
<p>import org.junit.Assert.assertNotNull<br />
import org.junit.Test<br />
import org.junit.runner.RunWith<br />
import org.robolectric.Robolectric<br />
import org.robolectric.RobolectricTestRunner</p>
<p>@RunWith(RobolectricTestRunner::class)<br />
class MainActivityTest {</p>
<p>    @Test<br />
    fun checkIfActivityIsSuccessfullyCreated() {<br />
        assertNotNull(Robolectric.setupActivity(MainActivity::class.java))<br />
    }<br />
}

Com o teste criado (e passando!), vamos configurar a geração dos dados de cobertura. Eu já escrevi dois posts sobre o assunto, então não vou me alongar muito sobre os detalhes de implementação aqui para não deixar o este post muito longo 🙂

Criei um script chamado coverage.gradle, que coloquei na pasta gradle/ do projeto (inicialmente, tinha colocado numa pasta chamada tools/, mas após algumas discussões a respeito, pareceu fazer mais sentido a primeira opção).

apply plugin: 'jacoco'</p>
<p>jacoco.toolVersion versions.jacoco</p>
<p>tasks.withType(Test) {<br />
    jacoco.includeNoLocationClasses = true<br />
}</p>
<p>def classes = fileTree(dir: &quot;$buildDir/tmp/kotlin-classes/debug&quot;)<br />
def sources = files(&quot;$projectDir/src/main/kotlin&quot;)<br />
def report = &quot;$buildDir/reports/jacoco/report.xml&quot;</p>
<p>task createCombinedCoverageReport(type: JacocoReport,<br />
        dependsOn: ['testDebugUnitTest', 'createDebugCoverageReport']) {</p>
<p>    sourceDirectories = sources<br />
    classDirectories = files(classes)<br />
    executionData = fileTree(dir: buildDir, includes: [<br />
            'jacoco/testDebugUnitTest.exec',<br />
            'outputs/code-coverage/connected/*coverage.ec'<br />
    ])</p>
<p>    reports {<br />
        xml.enabled = true<br />
        xml.destination file(report)</p>
<p>        html.enabled = true<br />
    }<br />
}

Bom, a parte de configuração do relatório em si é muito próxima do que expliquei nos dois posts sobre o assunto. Algumas coisas estão isoladas em variáveis (sourcesreport), pois vamos reutilizá-las já já.

Com isso, vamos incluir o arquivo no build.gradle do módulo app e habilitar a geração de cobertura de código para os testes instrumentados:

apply plugin: 'com.android.application'<br />
apply plugin: 'org.jetbrains.kotlin.android'</p>
<p>apply from: &quot;$rootDir/gradle/coverage.gradle&quot;</p>
<p>android {<br />
  ...<br />
  buildTypes {<br />
    debug {<br />
      testCoverageEnabled true<br />
      ...<br />
    }<br />
    ...<br />
  }<br />
}<br />
...

Para finalizar, vamos setar a versão do jacoco no classpath para a última versão, modificando o build.gradle na raiz do nosso projeto:

buildscript {</p>
<p>  ext.versions = [<br />
      'kotlin': '1.2.31',<br />
      'supportLibrary': '27.1.1',<br />
      'googleServices': '12.0.1',<br />
      'jacoco': '0.8.1'<br />
  ]</p>
<p>  repositories {<br />
    google()<br />
    jcenter()<br />
  }</p>
<p>  dependencies {<br />
    classpath 'com.android.tools.build:gradle:3.1.1'<br />
    classpath &quot;org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin&quot;<br />
    classpath 'com.google.gms:google-services:3.2.1'<br />
    classpath &quot;org.jacoco:org.jacoco.core:$versions.jacoco&quot;<br />
  }<br />
}

Feito isso, já podemos executar a task createCombinedCoverageReport e deveremos obter o relatório com incríveis 100% de cobertura, disponível dentro da pasta app/build/reports/jacoco

Perceba que ele foi gerado pela versão correta do Jacoco que configuramos, 0.8.1.

Com a métrica sendo gerada, é importante que ela esteja visível dentro do nosso fluxo de trabalho, para que possamos acompanhar a sua evolução ao longo do desenvolvimento do projeto. Para isso, precisamos “publicar” essa informação em algum lugar.

O Jenkins, por exemplo, oferece formas de acompanhar isso, caso que não ocorre com o CircleCI. Para isso, vamos utilizar um serviço externo, no caso o Coveralls. Durante o desenvolvimento, validei também o Codecov, porém ele não se comportou muito bem com o Kotlin. O Codecov irá se plugar ao nosso fluxo, validando a cobertura atual em cada Pull Request, e exibirá um histórico de como essa métrica evolui a cada commit.

Para que essa integração funcione, precisamos de enviar essa métrica para o Coveralls. Faremos isso através de um plugin. A configuração dele é bem simples:

// build.gradle<br />
buildscript {<br />
  ...<br />
  repositories {<br />
    google()<br />
    jcenter()<br />
    gradlePluginPortal()<br />
  }<br />
  dependencies {<br />
    classpath 'com.android.tools.build:gradle:3.1.1'<br />
    classpath &quot;org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin&quot;<br />
    classpath 'com.google.gms:google-services:3.2.1'<br />
    classpath &quot;org.jacoco:org.jacoco.core:$versions.jacoco&quot;<br />
    classpath 'org.kt3k.gradle.plugin:coveralls-gradle-plugin:2.8.2'<br />
  }<br />
}

No arquivo coverage.gradle, faremos a aplicação do plugin, bem como a sua configuração:

// coverage.gradle<br />
apply plugin: 'jacoco'<br />
apply plugin: 'com.github.kt3k.coveralls'</p>
<p>...</p>
<p>def classes = fileTree(dir: &quot;$buildDir/tmp/kotlin-classes/debug&quot;)<br />
def sources = files(&quot;$projectDir/src/main/kotlin&quot;)<br />
def report = &quot;$buildDir/reports/jacoco/report.xml&quot;</p>
<p>...</p>
<p>coveralls {<br />
    sourceDirs = sources.flatten()<br />
    jacocoReportPath = report<br />
}<br />

Com isso, o último passo é configurar a chave de upload dos relatórios no CI. Para isso, basta criarmos uma variável de ambiente chamada COVERALLS_REPO_TOKEN e colocar o valor fornecido no painel do Coveralls.

Feito isso, o último passo é editar o nosso arquivo de configuração do CI para incluir algumas coisas:

version: 2<br />
jobs:<br />
  build:<br />
    docker:<br />
      - image: circleci/android:api-27-alpha</p>
<p>    working_directory: ~/social-app</p>
<p>    environment:<br />
      JVM_OPTS: -Xmx3200m<br />
      CIRCLE_JDK_VERSION: oraclejdk8</p>
<p>    steps:<br />
      - checkout</p>
<p>      - restore_cache:<br />
          key: jars-{{ checksum &quot;build.gradle&quot; }}-{{ checksum &quot;app/build.gradle&quot; }}</p>
<p>      - run:<br />
          name: Accept licenses<br />
          command: yes | sdkmanager --licenses || true</p>
<p>      - run:<br />
          name: Decrypt release key<br />
          command: openssl aes-256-cbc -d -in distribution/release.keystore-cipher -out distribution/release.keystore -md sha256 -k $CIPHER_DECRYPT_KEY</p>
<p>      - run:<br />
          name: Setup Google Services JSON<br />
          command: |<br />
            mkdir -p app/src/debug/ &amp;amp;&amp;amp; touch app/src/debug/google-services.json<br />
            echo &quot;${JSON_FIREBASE_DEVELOPMENT}&quot; &amp;gt;&amp;gt; &quot;app/src/debug/google-services.json&quot;<br />
            mkdir -p app/src/release/ &amp;amp;&amp;amp; touch app/src/release/google-services.json<br />
            echo &quot;${JSON_FIREBASE_RELEASE}&quot; &amp;gt;&amp;gt; &quot;app/src/release/google-services.json&quot;</p>
<p>      - run:<br />
          name: Run Linters<br />
          command: ./gradlew check</p>
<p>      - run:<br />
          name: Run Tests and generate Code Coverage<br />
          command: ./gradlew createCombinedCoverageReport</p>
<p>      - run:<br />
          name: Upload code coverage data<br />
          command: ./gradlew coveralls</p>
<p>      - run:<br />
          name: Build<br />
          command: ./gradlew assemble assembleAndroidTest</p>
<p>      - store_artifacts:<br />
          path: app/build/reports/jacoco/createCombinedCoverageReport<br />
          destination: coverage-report</p>
<p>      - store_artifacts:<br />
          path: app/build/reports/tests/testDebugUnitTest<br />
          destination: local-test-report</p>
<p>      - save_cache:<br />
          paths:<br />
            - ~/.gradle<br />
          key: jars-{{ checksum &quot;build.gradle&quot; }}-{{ checksum &quot;app/build.gradle&quot; }}<br />

Aqui eu fiz algumas modificações. Primeiramente, separei as tasks de linters, testes e build (a partir da linha 35), adicionando a task que vai enviar os dados para o Coveralls (Upload code coverage data). Além disso, comecei a disponibilizar no CircleCI os relatórios e de cobertura e execução de testes (utilizando a configuração store_artifacts). Com isso, após a execução de uma build com sucesso, é possível navegar por esses artefatos, desde que esteja logado.

Ufa! É isso! Você pode conferir no repositório o resultado dessas mudanças. A partir de agora, a cobertura de código é exibida também no README do projeto e como uma etapa de verificação nos PRs abertos!

The Social Project: configurando o servidor de CI

Padrão

Se você ainda não conhece as motivações deste projeto, leia o primeiro artigo aqui.

No post anterior, criamos o repositório com um projeto vazio e configuramos o repositório com as branches persistentes que farão parte do nosso Git Flow.

Para essa tarefa, escolherei o CircleCI. Por quê? Primeiramente pelo seu suporte a Docker, o que facilita e muito na hora de fazer o setup do ambiente, caso a gente precise customizar o tooling. Além disso, ele possui uma cota gratuita generosa não só para projetos open-souce (que está sendo o caso deste daqui), mas também para projetos privados no Github ou Bitbucket.

Bom, primeiramente faremos o login no CircleCI com a nossa conta do Github e faremos o setup do nosso repositório.

Na tela seguinte, selecionamos uma máquina Linux e o Gradle como ferramenta de build. Nessa mesma tela, será oferecido um arquivo de configuração default, a ser colocado na pasta .circleci do repositório, com o nome de config.yml. O conteúdo inicial do arquivo é:

# Java Gradle CircleCI 2.0 configuration file<br />
#<br />
# Check https://circleci.com/docs/2.0/language-java/ for more details<br />
#<br />
version: 2<br />
jobs:<br />
  build:<br />
    docker:<br />
      # specify the version you desire here<br />
      - image: circleci/openjdk:8-jdk</p>
<p>      # Specify service dependencies here if necessary<br />
      # CircleCI maintains a library of pre-built images<br />
      # documented at https://circleci.com/docs/2.0/circleci-images/<br />
      # - image: circleci/postgres:9.4</p>
<p>    working_directory: ~/repo</p>
<p>    environment:<br />
      # Customize the JVM maximum heap limit<br />
      JVM_OPTS: -Xmx3200m<br />
      TERM: dumb</p>
<p>    steps:<br />
      - checkout</p>
<p>      # Download and cache dependencies<br />
      - restore_cache:<br />
          keys:<br />
          - v1-dependencies-{{ checksum &quot;build.gradle&quot; }}<br />
          # fallback to using the latest cache if no exact match is found<br />
          - v1-dependencies-</p>
<p>      - run: gradle dependencies</p>
<p>      - save_cache:<br />
          paths:<br />
            - ~/.gradle<br />
          key: v1-dependencies-{{ checksum &quot;build.gradle&quot; }}</p>
<p>      # run tests!<br />
      - run: gradle test</p>
<p>

A partir desse modelo, faremos algumas alterações. A primeira delas é mudar a imagem do docker para uma que já contenha todo o setup para que possamos buildar apps Android – temos uma lista das imagens mantidas pelo próprio CircleCI aqui. Além disso, configuramos a versão da JDK a ser utilizada e configuramos o cache de dependências (para que não precise ser baixadas novamente, caso não haja mudança nos arquivos do Gradle). Colocamos também uma task preventiva (Accept licenses) para evitar que o processo de build quebre por conta de licenças não aceitas.

Na task de build, configuramos a execução do Lint (através da task check), a compilação e execução de testes locais (através da task build) e, por enquanto, a compilação dos testes instrumentados (através da task assembleAndroidTest) – posteriormente faremos a delegação da execução desses testes para o Firebase Test Lab.

O arquivo final fica assim:

version: 2<br />
jobs:<br />
  build:<br />
    docker:<br />
      - image: circleci/android:api-27-alpha</p>
<p>    working_directory: ~/social-app</p>
<p>    environment:<br />
      JVM_OPTS: -Xmx3200m<br />
      CIRCLE_JDK_VERSION: oraclejdk8</p>
<p>    steps:<br />
      - checkout</p>
<p>      - restore_cache:<br />
          key: jars-{{ checksum &quot;build.gradle&quot; }}-{{ checksum &quot;app/build.gradle&quot; }}</p>
<p>      - run:<br />
          name: Accept licenses<br />
          command: yes | sdkmanager --licenses || true</p>
<p>      - run:<br />
          name: Build<br />
          command: ./gradlew clean check build assembleAndroidTest</p>
<p>      - save_cache:<br />
          paths:<br />
            - ~/.gradle<br />
          key: jars-{{ checksum &quot;build.gradle&quot; }}-{{ checksum &quot;app/build.gradle&quot; }}<br />

Feito isso, também colocarei um badge no README.md do repositório, indicando o atual status da nossa build. Tudo isso será então commitado em uma branch chamada rt/setup-ci e então um pull request será aberto.

Como sou administrador, eu poderia simplesmente ignorar o fluxo e eu mesmo mergear meu Pull Request. Porém, iria quebrar o fluxo que eu mesmo estou propondo! 🙂

Para fazer o review do meu PR, adicionei a Elessandra como revisora. No caso, a pessoa que revisa o PR pode fazer apontamentos e, caso esteja tudo certo, aprovar e mergear.

Como nem tudo são flores, eu commitei o arquivo com alguns erros de digitação na primeira tentativa. Como ainda não tínhamos o CI integrado para validar o arquivo de configuração, os olhos humanos deixaram passar. Com isso, fui obrigado a fazer outro PR revertendo a modificação. Pela própria interface do Github é possível solicitar desfazer o PR.

Corrigidos os erros (e, após algumas tentativas), finalmente consegui configurar o CI corretamente.

Como foram vários commits de tentativa e erro, o PR ficou com vários commits. Para esse caso, o ideal é que se faça o Squash (juntar todos os commits em um único), evitando assim que o histórico do Git seja poluído.

Por fim, após o merge é importante excluir a branch de origem, já que ela não é uma branch persistente. Assim, mantemos o repositório limpo e organizado 🙂

Por hoje é isso. O repositório já com as modificações está disponível aqui.