JaCoCo 🏠

Os slides acima apresentam a cobertura de código com o JaCoCo num projeto Quarkus. O exemplo é o cadastro de livros em exemplos/hexagonal: a pasta se chama hexagonal, e o sistema é a livraria (Book, BooksService, BookResource). Nas seções a seguir, a mesma ideia aparece com mais calma e, nos exercícios, você acende o relatório desse projeto.

A planta da casa

Imagine o código como a planta de uma casa, à noite. Cada método é um cômodo. Cada if é uma porta. Os testes são quem entra e acende a luz.

O JaCoCo (Java Code Coverage) desenha essa planta: onde alguém passou, a luz fica acesa. O relatório é esse desenho.

  • Cômodo verde: o método foi percorrido por inteiro.
  • Interruptor âmbar: alguém entrou, mas deixou uma porta fechada.
  • Cômodo escuro: nenhum teste chegou lá.

Esse é o argumento do slide A planta da casa. A moral fica para o fim da página: acender todas as luzes não prova que a casa está em ordem. Cobertura mede o que foi executado. Quem diz se o resultado está certo é o assert.

O que a planta mostra

O relatório HTML mostra vários contadores. Você não precisa decorar a fórmula de cada um. Precisa saber o que está olhando. O slide O que ele mede resume os cinco que mais aparecem.

  • Instrução. O passo mais curto no chão: uma instrução de bytecode. Várias instruções cabem numa única linha de Java.
  • Linha. O corredor. Uma linha conta como coberta quando pelo menos uma instrução dela rodou.
  • Ramo. Cada porta. Um if, um else e cada saída de um switch são ramos. O losango desenhado na linha é esse contador.
  • Método. O cômodo. Entra na conta quando o método é chamado.
  • Classe. A ala da casa. Entra na conta quando pelo menos um método dela roda.

Há ainda a coluna de complexidade (ciclomática): um número de caminhos independentes. Ela ajuda a perceber um método cheio de portas. Não é a meta dos exercícios desta página.

Linha e ramo não são a mesma coisa

A linha do if acende assim que a condição é avaliada, mesmo que só um lado tenha sido tomado. O ramo só fica verde quando as duas portas abrem. Por isso a cobertura de linhas pode parecer boa enquanto a de ramos ainda está pela metade. O slide O losango âmbar usa a validação de autor do Book, no cadastro de livros:

if (author == null || author.isBlank()) {
    throw new InvalidBookException("Author cannot be blank");
}

Um teste unitário com autor "" executa a linha do if e o throw. O losango continua âmbar: a porta author == null não abriu, porque || tem duas entradas. O BooksService.add tem a mesma forma de porta em opt.isPresent(). O BooksServiceTest já acende as duas: ISBN novo e ISBN repetido.

Como ler as cores

Abra o index.html do relatório e entre na classe. O JaCoCo pinta o fonte assim (slide As cores do relatório):

  • Verde: tudo o que está naquela linha foi executado.
  • Amarelo: a linha rodou, mas algum ramo dela ficou de fora. O losango ao lado da linha é a porta ainda fechada.
  • Vermelho: nenhum teste passou por ali.

No topo da página, as barras repetem os contadores da seção anterior. Use a de ramos quando quiser saber se as portas foram abertas, e a de linhas quando quiser uma leitura rápida dos corredores.

Por que a extensão, e não o plugin sozinho

Num projeto Maven comum, o JaCoCo entra como jacoco-maven-plugin e faz dois trabalhos, cada um num goal:

  • prepare-agent sobe um agente Java antes dos testes. Esse agente reescreve o bytecode no momento em que a classe é carregada e anota, em um arquivo .exec, cada instrução que rodou. O Surefire recebe essa configuração pela propriedade argLine.
  • report lê o .exec depois dos testes e monta o HTML. Sem esse goal, os dados existem, mas a planta não aparece no navegador.

Esse arranjo funciona quando o teste e o código de produção são carregados pelo mesmo classloader, o da JVM do Surefire. No cadastro de livros, o BooksServiceTest é esse caso: ele monta o BooksService na mão, sem subir a aplicação. A classe entra pela porta da frente, o agente está nessa porta e acende a luz.

O @QuarkusTest muda a porta. O Quarkus sobe a aplicação dentro do teste e carrega as classes já aumentadas pelo próprio classloader, QuarkusClassLoader. O agente pendurado pelo prepare-agent continua na porta da frente. Ele não acompanha o que entra pela porta de serviço, então o teste pode passar e o cômodo da aplicação continuar escuro no relatório. O slide Por que a extensão no Quarkus? resume esse desvio.

A extensão quarkus-jacoco fica do lado de dentro dessa subida. Ela faz, para o @QuarkusTest, o que os dois goals fariam à mão: instrumenta a classe quando o QuarkusClassLoader a carrega, grava target/jacoco-quarkus.exec e gera o HTML em target/jacoco-report. No cadastro de livros, quem passa por essa porta é o BookResourceTest. O BooksServiceTest instancia BooksService direto, sem @QuarkusTest, então a extensão sozinha não acende esses testes. Os exercícios desta página são unitários e usam o jacoco-maven-plugin, que enxerga essa porta da frente.

Os dois juntos, sem a configuração da seção Como ligar neste projeto, instrumentam a mesma classe duas vezes. O segundo passe encontra o bytecode que o primeiro já marcou e o teste quebra com erro de classe já instrumentada. Com exclClassLoaders, a extensão cobre o @QuarkusTest e o plugin cobre o JUnit que não sobe o Quarkus, gravando os dois no mesmo arquivo.

Como ligar neste projeto

Este projeto é Quarkus, então a forma certa de ligar o JaCoCo muda em relação a um Maven comum. O caminho, em exemplos/hexagonal, é a extensão quarkus-jacoco junto com o jacoco-maven-plugin. A extensão cobre os testes @QuarkusTest (BookResourceTest). O plugin cobre os testes JUnit comuns (BooksServiceTest, onde também está a validação de Book) e grava tudo no mesmo arquivo de execução. O @{argLine} que já está no Surefire e no Failsafe deve permanecer: é ele que recebe o agente do JaCoCo. O slide Como ligar resume a dependência; o arranjo completo é o desta seção.

Dependência

A versão vem do BOM do Quarkus já importado no pom.xml (quarkus.platform.version, 3.34.1 neste repositório). Não declare versão na dependência.

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-jacoco</artifactId>
    <scope>test</scope>
</dependency>

Propriedade e plugin

Em <properties>:

<jacoco.version>0.8.15</jacoco.version>

A 0.8.15 é a release estável atual e cobre o Java 25 deste pom.xml.

Em <build><plugins>, ao lado do Surefire:

<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>${jacoco.version}</version>
    <executions>
        <execution>
            <id>default-prepare-agent</id>
            <goals>
                <goal>prepare-agent</goal>
            </goals>
            <configuration>
                <exclClassLoaders>*QuarkusClassLoader</exclClassLoaders>
                <destFile>${project.build.directory}/jacoco-quarkus.exec</destFile>
                <append>true</append>
            </configuration>
        </execution>
    </executions>
</plugin>

O exclClassLoaders faz o agente do plugin ignorar as classes carregadas pelo Quarkus. Sem isso, as mesmas classes são instrumentadas duas vezes e o build quebra. O destFile é o arquivo padrão da extensão (target/jacoco-quarkus.exec), então a cobertura dos dois tipos de teste fica no mesmo lugar. O relatório HTML é gerado pela extensão, desde que rode pelo menos um @QuarkusTest.

Gerar o relatório

./mvnw verify

O HTML fica em target/jacoco-report/index.html. Os dados brutos ficam em target/jacoco-quarkus.exec. Esse diretório é o padrão de quarkus.jacoco.report-location. O slide Onde fica o relatório lista os dois arquivos.

Para limitar o que entra no relatório, use src/main/resources/application.properties (ou o profile de teste):

quarkus.jacoco.excludes=**/BookRequest.class

Os curingas seguem o formato do JaCoCo, por exemplo **/domain/model/*. BookRequest é um DTO: não é a regra de Book, então pode sair do relatório. Nos exercícios, a exclusão equivalente fica no report do jacoco-maven-plugin.

Limite mínimo

Se quiser falhar o build quando a cobertura ficar abaixo de um piso, inclua outra <execution> no mesmo plugin, apontando para o mesmo arquivo:

<execution>
    <id>jacoco-check</id>
    <goals>
        <goal>check</goal>
    </goals>
    <phase>verify</phase>
    <configuration>
        <dataFile>${project.build.directory}/jacoco-quarkus.exec</dataFile>
        <rules>
            <rule>
                <element>BUNDLE</element>
                <limits>
                    <limit>
                        <counter>LINE</counter>
                        <value>COVEREDRATIO</value>
                        <minimum>0.80</minimum>
                    </limit>
                </limits>
            </rule>
        </rules>
    </configuration>
</execution>

O BookResourceIT (@QuarkusIntegrationTest) não entra nesse relatório: skipITs está true no pom.xml. Cobertura em modo nativo também não é suportada.

Os exercícios mais abaixo continuam unitários, só com BooksServiceTest, para não subir a API. O HTML que eles abrem é o do goal report do plugin, em target/site/jacoco/index.html. O arranjo desta seção é o do projeto inteiro, quando extensão e plugin rodam juntos.

Exercícios práticos: o cadastro de livros

Os exercícios acontecem no projeto que já existe, em exemplos/hexagonal. A pasta tem nome de arquitetura; o sistema é o cadastro de livros. Você não cria outro projeto e não sobe a API. Tudo é teste unitário em BooksServiceTest, no grupo BookValidation, chamando Book.of. Os exercícios vão do mais simples ao mais exigente. Resolva na ordem. Depois de cada um, rode o comando abaixo, abra target/site/jacoco/index.html e só avance quando o relatório mostrar o que o enunciado pede. O slide Exercícios é a lista curta.

./mvnw test -Dtest=BooksServiceTest -Dskip.frontend=true

-Dtest=BooksServiceTest deixa de fora o BookResourceTest. Sem esse teste de API, o Docker e o MySQL não entram no caminho.

Preparação

Entre na pasta do cadastro de livros.

cd exemplos/hexagonal
code .

O teste unitário já está em src/test/java/dev/ifrs/hexagonal/domain/service/BooksServiceTest.java. Ele monta BooksService com um repositório em memória. A validação de Book fica na classe interna BookValidation. Os métodos novos entram aí, ao lado de shouldRejectBlankTitle.

Exercício 1: localizar o ramo do autor

Adicione o jacoco-maven-plugin ao pom.xml de exemplos/hexagonal. O prepare-agent instrumenta as classes carregadas pelo Surefire. O report, na fase test, escreve o HTML. O Surefire desse projeto já usa @{argLine}, então o agente entra no teste unitário.

<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.8.15</version>
    <executions>
        <execution>
            <goals>
                <goal>prepare-agent</goal>
            </goals>
        </execution>
        <execution>
            <id>report</id>
            <phase>test</phase>
            <goals>
                <goal>report</goal>
            </goals>
        </execution>
    </executions>
</plugin>

Rode ./mvnw test -Dtest=BooksServiceTest -Dskip.frontend=true e abra target/site/jacoco/index.html. Em Book.validate, a linha if (author == null || author.isBlank()) fica amarela: os testes que já existem sempre mandam um autor preenchido, então o throw continua vermelho.

Exercício 2: cobrir o autor em branco

Dentro de BookValidation, acrescente o teste do autor vazio. VALID_ISBN já está nessa classe.

@Test
void shouldRejectBlankAuthor() {
    assertThrows(InvalidBookException.class,
            () -> Book.of(VALID_ISBN, "Clean Code", "", 2008, 5));
}

Rode o mesmo ./mvnw test -Dtest=BooksServiceTest -Dskip.frontend=true. O throw do autor em branco fica verde. O ramo do || continua parcial: author == null ainda não foi executado. Anote o percentual de linhas e o de ramos de validate. Você usa esses dois números no exercício 6.

Exercício 3: cobrir author == null

Acrescente o teste em que o autor é nulo. Os dois lados de author == null || author.isBlank() passam a contar como cobertos e o ramo fica verde.

@Test
void shouldRejectNullAuthor() {
    assertThrows(InvalidBookException.class,
            () -> Book.of(VALID_ISBN, "Clean Code", null, 2008, 5));
}

Exercício 4: cobrir publicationYear < 1

shouldRejectFuturePublicationYear já executa o throw com o ano 9999. O outro lado, publicationYear < 1, ainda não roda. Cubra esse ramo com um ano zero.

@Test
void shouldRejectPublicationYearBeforeOne() {
    assertThrows(InvalidBookException.class,
            () -> Book.of(VALID_ISBN, "Clean Code", "Robert Martin", 0, 5));
}

O ramo de publicationYear < 1 || publicationYear > currentYear fica verde, e o teste continua conferindo que Book.of rejeita o ano.

Exercício 5: excluir BookRequest do relatório

BookRequest aparece no relatório sem cobertura: nenhum teste unitário o carrega. É a forma do JSON, não a regra do livro. No report do plugin, exclua a classe e rode o teste outra vez.

<execution>
    <id>report</id>
    <phase>test</phase>
    <goals>
        <goal>report</goal>
    </goals>
    <configuration>
        <excludes>
            <exclude>**/BookRequest.class</exclude>
        </excludes>
    </configuration>
</execution>

BookRequest sai do relatório. Book permanece, com a cobertura que BookValidation produziu.

Exercício 6: uma frase sobre linha e ramo

Sem escrever código novo, volte aos percentuais que você anotou no exercício 2, depois só do autor vazio, antes do autor nulo. Escreva uma frase dizendo por que a cobertura de linhas de validate e a de ramos não coincidem. A pista está na seção Linha e ramo não são a mesma coisa: a linha do if já conta como executada quando só um lado do || rodou.

Teste seus conhecimentos 🧠

Revise o texto e os slides e responda às questões teóricas abaixo.

Referências

Rodrigo Prestes Machado
CC BY 4.0 DEED

Copyright © 2026 RPM Hub. Distributed by CC-BY-4.0 license.

This site uses Just the Docs, a documentation theme for Jekyll.