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, umelsee cada saída de umswitchsã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-agentsobe 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 propriedadeargLine.reportlê o.execdepois 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
- JaCoCo. Documentação. Disponível em: https://www.jacoco.org/jacoco/trunk/doc/.
- Quarkus. Measuring the coverage of your tests. Disponível em: https://quarkus.io/guides/tests-with-coverage/.

CC BY 4.0 DEED