Você tem 100% de cobertura de linhas para testes unitários. Mesmo assim, um bug é lançado.
Isso acontece porque a cobertura de linhas mede quais linhas de código seus testes executam , e não se seus testes são realmente capazes de detectar um defeito. Um teste que chama um método, mas nunca verifica nada, elevará sua cobertura para 100% sem detectar absolutamente nada. Esse não é um caso isolado. É uma falha sistemática na forma como a maioria das equipes mede a qualidade dos testes.
O teste de mutação é a resposta. Ele funciona introduzindo automaticamente pequenos bugs no seu código, chamados de mutantes , e executando seu conjunto de testes em cada um deles. Se os testes falharem, o mutante é eliminado. Se os testes forem aprovados, o mutante sobreviveu e você encontrou uma falha no seu conjunto de testes.
Uma importante ressalva antes de prosseguirmos: o PIT não exige que você escreva uma nova categoria de testes. Ele é executado sobre os testes JUnit que você já possui. Pense nele como uma camada de qualidade sobreposta ao seu conjunto de testes existente. Ele avalia o desempenho desses testes, em vez de substituí-los ou complementá-los com algo novo.
O PIT (PITest) é a ferramenta padrão para testes de mutação na JVM. Ele é rápido em comparação com sistemas de teste de mutação mais antigos, integra-se diretamente com Maven e Gradle e gera um relatório HTML que pode ser visualizado em um navegador. Este artigo aborda tudo o que você precisa para configurar o PIT em um projeto Spring Boot com Maven e JUnit 5, ler o relatório, definir limites de qualidade e executá-lo em CI.
Quer o código? O projeto de demonstração completo está disponível no GitHub: loiane/spring-boot-pit-demo
Neste post, abordaremos:
- Por que a cobertura de linha não é suficiente e o que os testes de mutação medem em vez disso.
- Como funcionam os mutantes PIT e quais mutadores estão ativos por padrão.
- O que são mutações equivalentes e por que uma pontuação de mutação de 100% não é realista?
- Configurando o PIT em um projeto Spring Boot + Maven + JUnit 5 (incluindo a pegadinha do JUnit 5 que quebra a maioria das configurações iniciais)
- Executando o PIT por trás de um perfil do Maven para manter as compilações normais rápidas.
- Leitura e implementação do relatório PIT HTML
- Executando o PIT no GitHub Actions com arquivamento de artefatos
- Otimização de desempenho para projetos reais
Por que a cobertura de linha não é suficiente
A cobertura de linhas responde a uma pergunta: meu conjunto de testes executou esta linha? Ela não responde: meu conjunto de testes detectaria um defeito nesta linha?
A lacuna é real. Considere este método de serviço:
public double applyDiscount(double price, double discountPercent) {
return price - (price * discountPercent / 100);
}
E este teste:
@Test
void testApplyDiscount() {
service.applyDiscount(100.0, 10.0);
// no assertion
}
Cobertura de linha: 100%. Detecção de defeitos: 0%. Se alguém alterar *a /fórmula, o teste ainda será aprovado.
O teste de mutação detecta isso. O PIT substituiria *por /, executaria o teste novamente e constataria que ele é aprovado. O mutante sobrevive. O relatório revela a lacuna.
A definição formal: pontuação de mutação = mutações eliminadas / total de mutações. Uma pontuação de 80% significa que 20% dos bugs injetados não foram detectados pelo seu conjunto de testes.
Como funcionam os mutantes PIT
O PIT modifica o bytecode compilado, não os arquivos de origem. Isso torna sua execução rápida e compatível com o ciclo de vida de compilação do Java, sem a necessidade de reescrita do código-fonte.
Para cada classe em teste, o PIT gera um conjunto de mutantes aplicando um operador por vez. Em seguida, coleta a cobertura de cada linha de teste para determinar quais testes existentes cobrem a linha mutada e executa apenas esses testes em cada mutante. Sem novas classes de teste, sem configuração separada do executor de testes. O PIT descobre e reutiliza os testes JUnit 5 já presentes no seu classpath. A maioria das execuções modernas leva minutos, não horas, para serviços típicos.
Três resultados são possíveis para cada mutante:
| Resultado | Significado |
|---|---|
| Morto | Pelo menos um teste detectou a falha e falhou. |
| Sobreviveu | Todos os testes foram aprovados com o bug presente. |
| Sem cobertura | Nenhum teste executa a linha modificada. |
Um mutante sobrevivente e um mutante sem cobertura indicam uma lacuna nos testes, mas são problemas diferentes. Sem cobertura significa que a linha não foi alcançada. Sobrevivente significa que a linha foi alcançada, mas os testes não afirmam nada significativo sobre sua saída.
Os modificadores padrão que você deve conhecer
O PIT é fornecido com vários grupos de mutadores. O DEFAULTSgrupo permanece ativo a menos que você o substitua. Não é necessário configurar isso para começar, mas saber o que o PIT está alterando ajuda a entender o relatório.
| Mutador | O que isso muda | Exemplo |
|---|---|---|
CONDITIONALS_BOUNDARY |
Substitui <por <=, >por >=, etc. |
if (a < b)→if (a <= b) |
NEGATE_CONDITIONALS |
Alterna ==entre !=, <para >=, etc. |
if (a == b)→if (a != b) |
MATH |
Operadores aritméticos de troca | a + b→a - b |
INCREMENTS |
Inversões ++e --em variáveis locais |
i++→i-- |
VOID_METHOD_CALLS |
Remove completamente as chamadas a métodos vazios. | validate(input)→ (removido) |
NULL_RETURNS |
Retornos nullde métodos que retornam valores não nulos |
return user→return null |
EMPTY_RETURNS |
Retorna um valor vazio para coleções e opcionais. | return list→return emptyList() |
FALSE_RETURNS |
Retornos falsede métodos booleanos |
return isValid→return false |
TRUE_RETURNS |
Retornos truede métodos booleanos |
return isValid→return true |
Além disso DEFAULTS, o PIT fornece um STRONGERgrupo que adiciona mais mutadores de valor de retorno e um ALLgrupo que habilita todos os operadores experimentais. Os mantenedores desencorajam explicitamente o uso ALLna prática, porque ele gera muitas mutações equivalentes e dificulta a tomada de decisões com base nos relatórios.
Para usar o conjunto mais robusto:
<configuration>
<mutators>
<mutator>STRONGER</mutator>
</mutators>
</configuration>
Mutações Equivalentes: Por que 100% não é uma meta realista
Nem toda mutação sobrevivente representa uma lacuna no conjunto de testes. Algumas mutações são equivalentes : o código modificado produz exatamente o mesmo comportamento observável que o original, portanto nenhum teste jamais poderá eliminá-las.
Um exemplo comum é uma otimização focada exclusivamente no desempenho, como pré-dimensionar uma coleção:
// the "+ 1" is a capacity hint, not part of the result
Map<String, String> cache = new HashMap<>(items.size() + 1);
MATHO mutador do PIT pode ser alterado + 1para - 1. O mapa ainda armazena exatamente as mesmas entradas e retorna exatamente os mesmos resultados, e apenas seu comportamento interno de redimensionamento muda, o que é invisível para qualquer asserção funcional. O comportamento observável é idêntico, portanto, nenhum teste que você escrever jamais eliminará esse mutante.
Isso é importante para definir limites. Uma taxa de mutação de 80 a 85% é um resultado excelente para a maioria dos códigos de produção. Tentar atingir 100% geralmente significa escrever testes frágeis que existem apenas para eliminar mutações específicas, e não para validar o comportamento.
A abordagem prática: trate as mutações sobreviventes como uma lista de pendências priorizada. Leia o relatório, decida quais sobreviventes representam lacunas reais, adicione afirmações específicas para essas e aceite que algumas mutações sobreviventes serão sempre equivalentes.
Para tornar as mutações equivalentes mais visíveis, você pode adicionar um formato de saída XML e relatórios de diferenças entre as execuções. Mais detalhes na seção de CI.
Configurando o PIT em um projeto Spring Boot
O Projeto de Demonstração
A demonstração é uma API REST do Spring Boot com três camadas: uma camada de desenvolvimento ProductController, uma ProductServicecamada de processamento e uma camada de aplicação ProductRepository. O código-fonte completo está disponível no GitHub em loiane/spring-boot-pit-demo .
Estrutura do projeto:
src/
main/java/com/loiane/pit/
controller/
ProductController.java
service/
ProductService.java
model/
Product.java
repository/
ProductRepository.java
test/java/com/loiane/pit/
controller/
ProductControllerTest.java
service/
ProductServiceTest.java
Pré-requisitos
- Java 21 (LTS). A análise de bytecode do PIT ainda não suporta arquivos de classe do Java 26, portanto, a demonstração é direcionada ao Java 21. Atualize esta página
java.versionassim que sua versão do PIT suportar um JDK mais recente. - Maven 3.8 ou superior (a versão de demonstração inclui o Maven Wrapper, portanto
./mvnwfunciona sem uma instalação local do Maven) - A demonstração é executada no Spring Boot 4.1. O JUnit 5 é obtido transitivamente através dos starters de teste do Spring Boot (
spring-boot-starter-webmvc-testespring-boot-starter-data-jpa-test). No Spring Boot 3.x, ospring-boot-starter-teststarter único funciona da mesma maneira para PIT.
A pegadinha do JUnit 5
O PIT não detecta automaticamente testes JUnit 5 por padrão. Se você adicionar o pitest-mavenplugin sem a ponte JUnit 5, o PIT reportará 0 testes encontrados e falhará na compilação ou ignorará a mutação completamente. Esta é a razão mais comum para que a primeira instalação do PIT não faça nada silenciosamente.
A solução: declare pitest-junit5-plugincomo uma dependência dentro do bloco do plugin , e não como uma dependência do projeto.
Configuração mínima de trabalho
Adicione o seguinte ao <build><plugins>seu arquivo pom.xml:
<plugin>
<groupId>org.pitest</groupId>
<artifactId>pitest-maven</artifactId>
<version>1.20.4</version>
<dependencies>
<!-- Required for JUnit 5: PIT finds 0 tests without this -->
<dependency>
<groupId>org.pitest</groupId>
<artifactId>pitest-junit5-plugin</artifactId>
<version>1.2.3</version>
</dependency>
</dependencies>
</plugin>
Execute o programa uma vez para verificar se a configuração funciona:
mvn test-compile org.pitest:pitest-maven:mutationCoverage
O relatório é exibido em target/pit-reports/. Abra index.htmlem um navegador. Se você vir uma tabela de classes com pontuações de mutação, está tudo pronto.
Configuração pronta para produção
A configuração mínima é suficiente para explorar. Para um projeto real, você precisará de escopo explícito, limites que interrompam a compilação, saída em HTML e XML (para comparações em CI) e histórico incremental para acelerar as execuções locais.
<plugin>
<groupId>org.pitest</groupId>
<artifactId>pitest-maven</artifactId>
<version>1.20.4</version>
<dependencies>
<dependency>
<groupId>org.pitest</groupId>
<artifactId>pitest-junit5-plugin</artifactId>
<version>1.2.3</version>
</dependency>
</dependencies>
<configuration>
<!-- Scope to business logic: exclude DTOs, config, generated code -->
<targetClasses>
<param>com.loiane.pit.service.*</param>
<param>com.loiane.pit.controller.*</param>
</targetClasses>
<targetTests>
<param>com.loiane.pit.*</param>
</targetTests>
<!-- Use multiple threads to parallelize mutation analysis -->
<threads>4</threads>
<!-- Fail the build if mutation score falls below this percentage -->
<mutationThreshold>80</mutationThreshold>
<!-- Fail the build if test strength falls below this percentage -->
<!-- Test strength = killed / (killed + survived), excludes no-coverage -->
<testStrengthThreshold>90</testStrengthThreshold>
<!-- Use decimal precision to avoid the integer rounding blind spot -->
<thresholdPrecision>1</thresholdPrecision>
<!-- HTML for humans, XML for CI tooling and report diffing -->
<outputFormats>
<outputFormat>HTML</outputFormat>
<outputFormat>XML</outputFormat>
</outputFormats>
<!-- Speed up repeated local runs by reusing mutation history -->
<withHistory>true</withHistory>
<!-- Exclude logging calls from mutation. PIT already avoids common -->
<!-- logging frameworks by default; list them explicitly to be safe -->
<!-- and to document the intent for your team. -->
<avoidCallsTo>
<avoidCallsTo>java.util.logging</avoidCallsTo>
<avoidCallsTo>org.slf4j</avoidCallsTo>
</avoidCallsTo>
</configuration>
</plugin>
Uma observação sobrethresholdPrecision : a comparação de limite padrão usa porcentagens inteiras. Um projeto com uma pontuação de mutação de 80,49% com um limite de 80passa, porque 80,49 arredonda para 80. Um projeto com 79,51% também passa (arredonda para 80). Isso representa uma diferença de 1%. Definir thresholdPrecisionpara 1compara com uma casa decimal e elimina essa diferença.
Uma observação sobrewithHistory : isso instrui o PIT a armazenar em cache os resultados das mutações entre as execuções localmente. Se uma classe e seus testes não foram alterados, o PIT ignora a execução repetida dessas mutações. Em um projeto de tamanho médio, isso pode reduzir o tempo de execução em 50 a 70% para execuções incrementais. É útil apenas localmente, já que a integração contínua (CI) começa do zero.
Excluindo ruído
Nem todo o código deve ser alterado. DTOs, registros, classes de configuração e a autoconfiguração do Spring Boot são exemplos de elementos que devem ser excluídos. Alterá-los gera alterações equivalentes e aumenta o relatório sem adicionar nenhum sinal.
<excludedClasses>
<param>com.loiane.pit.model.*</param>
<param>com.loiane.pit.*Application</param>
<param>com.loiane.pit.config.*</param>
</excludedClasses>
Exclua também métodos que não podem ser testados de forma significativa por mutação, como `__init__` hashCode, equals`__get__` e `__get__` toStringem tipos de valor simples:
<excludedMethods>
<param>hashCode</param>
<param>equals</param>
<param>toString</param>
</excludedMethods>
Usando um perfil do Maven para manter as compilações normais rápidas
O teste de mutação é lento em comparação com a execução de um teste unitário. Em um projeto de tamanho médio, uma execução completa do PIT pode levar de 3 a 10 minutos. Você não quer isso em todos os testes mvn test.
O padrão recomendado é isolar o PIT por trás de um perfil do Maven. Mova toda a configuração do plugin para dentro de um <profiles>bloco:
<profiles>
<profile>
<id>pitest</id>
<build>
<plugins>
<plugin>
<groupId>org.pitest</groupId>
<artifactId>pitest-maven</artifactId>
<version>1.20.4</version>
<dependencies>
<dependency>
<groupId>org.pitest</groupId>
<artifactId>pitest-junit5-plugin</artifactId>
<version>1.2.3</version>
</dependency>
</dependencies>
<configuration>
<!-- full configuration here -->
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
Agora a configuração normal permanece inalterada:
mvn test # fast, no mutation analysis
mvn -Ppitest test # runs unit tests + full mutation analysis
No CI, ative o perfil apenas na etapa de teste de mutação.
Lendo o Relatório PIT
O relatório HTML está localizado em target/pit-reports/. Abra index.html.
Página de visão geral
A tabela de resumo mostra todas as classes em escopo com quatro métricas:
| Coluna | O que mede |
|---|---|
| Linha % | Cobertura de linha padrão |
| Mutação % | Pontuação de mutação (mortos / total) |
| Força do teste | Morto / (morto + sobrevivente), exclui mutantes sem cobertura |
| Mutações | Contagem bruta: mortos, sobreviventes, sem cobertura, tempo limite excedido |
Ordene por porcentagem de mutação em ordem crescente para encontrar primeiro as classes com menor índice de testes.

O projeto de demonstração alcançou 92% de cobertura de mutações e 92% de robustez nos testes. As finas faixas rosa na com.loiane.pit.servicelinha indicam as mutações remanescentes que ainda aguardam uma confirmação.
Análise detalhada em nível de aula
Clique em qualquer nome de classe para abrir a visualização do código-fonte. Cada linha está anotada:
- Verde claro : linha coberta por testes
- Verde escuro : todos os mutantes desta linhagem foram mortos.
- Rosa claro : linha não coberta pelos testes
- Rosa escuro : pelo menos um mutante sobreviveu nesta linha coberta.
As linhas rosa-escuras são as que você deve observar com atenção. Elas indicam que seus testes alcançam o código, mas não o validam suficientemente bem para detectar uma alteração simples.

Na ProductServiceimagem de origem acima, as linhas verdes estão completamente eliminadas, enquanto as duas linhas rosas (a discountPercentlinha de guarda em applyDiscounte a quantitylinha de guarda em bulkDiscountRate) possuem um CONDITIONALS_BOUNDARYmutante sobrevivente cada uma. Ambas são alcançadas pelos testes, mas nunca confirmadas exatamente no limite.
Agindo com base no relatório
Utilize esta tabela de decisão ao analisar os sobreviventes:
| Status | Significado | Ação recomendada |
|---|---|---|
| Morto | O teste detectou a falha. | Nenhuma ação necessária |
| Sobreviveu | Os testes foram aprovados, mas com um bug presente. | Adicionar ou reforçar uma afirmação |
| Sem cobertura | Nenhum teste chega a essa linha | Adicione um teste que exercite o caminho. |
| Tempo limite excedido | É provável que a mutação tenha causado um loop infinito. | Geralmente é ruído; verifique se for frequente. |
Para mutantes sobreviventes, clique na linha para ver a mutação exata. O PIT mostra o que mudou. Escreva a asserção mínima que detectaria essa mudança específica. Na maioria dos casos, isso significa verificar o valor de retorno do método que está sendo testado, e não apenas verificar se ele foi chamado.
Exemplo: no projeto de demonstração, o PIT reporta uma taxa de mutação de 92% com três sobreviventes, todos CONDITIONALS_BOUNDARYmutantes em verificações de limite exato (o discountPercent > 100guarda em applyDiscount, o quantity < 0guarda em bulkDiscountRatee o stockQuantity < 0guarda em validate). Cada um significa que os testes exercitam o método, mas nunca verificam o comportamento exatamente no valor limite. Adicione um teste que forneça uma entrada exatamente no limite e verifique a saída esperada, e o mutante será eliminado.
Executando o PIT no GitHub Actions
Este fluxo de trabalho executa o PIT como uma etapa separada em cada solicitação de push e pull direcionada a main. O relatório HTML é arquivado como um artefato de compilação para que os revisores possam inspecioná-lo sem precisar executar a análise novamente.
Criar .github/workflows/mutation.yml:
name: Mutation Testing
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
mutation:
name: PIT Mutation Analysis
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Java
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
cache: maven
- name: Compile and run mutation tests
run: ./mvnw -Ppitest -B test-compile org.pitest:pitest-maven:mutationCoverage
- name: Archive mutation report
if: always()
uses: actions/upload-artifact@v4
with:
name: pit-mutation-report
path: target/pit-reports/
retention-days: 14
Algumas decisões de design neste fluxo de trabalho que merecem destaque:
if: always()Na etapa de arquivamento, o relatório é carregado mesmo quando a compilação falha devido a uma violação de limite. Isso é intencional, pois o objetivo é ler o relatório para entender por que o limite foi violado, e não apenas saber que isso aconteceu.retention-days: 14O sistema mantém o relatório por duas semanas, o que é suficiente para a maioria dos ciclos de revisão de relações públicas sem acumular custos de armazenamento.cache: mavenReutiliza o repositório Maven local entre as execuções para evitar o download repetido das dependências do PIT e do Spring Boot a cada execução.- A
withHistoryopção na configuração do plugin é útil localmente, mas não em CI. Os executores de CI começam do zero, então não há histórico para reutilizar. Para obter velocidade em CI, confie notargetClassesescopo ethreadsem vez disso.
Falha na compilação devido a violações de limite
Com as opções `–no-limit` mutationThresholde `–no- testStrengthThresholdlimit` definidas no plugin, a mutationCoveragetarefa será encerrada com um código de saída diferente de zero quando a pontuação cair abaixo do limite. O GitHub Actions trata códigos de saída diferentes de zero como falhas, portanto, nenhuma configuração adicional é necessária e a tarefa ficará vermelha.
Comece com um limite de 70% e aumente-o gradualmente à medida que adicionar asserções. Definir um limite de 80% logo no primeiro dia, em um código legado com baixa cobertura, só criará atrito. O limite é um piso, não uma meta.
Desempenho: Mantendo o PIT Rápido em Projetos Reais
Em um projeto pequeno, o PIT é executado em menos de um minuto. Em uma aplicação Spring Boot de médio porte, ele pode levar de 5 a 15 minutos sem ajustes. Aqui estão os controles mais eficazes:
Utilize múltiplas threads. O PIT paraleliza a análise de mutações entre threads. Configurar isso <threads>4</threads>em um executor de CI de 4 núcleos normalmente reduz o tempo de execução pela metade. O número ideal geralmente fica entre 1 e o número de CPUs disponíveis.
Defina o escopo targetClassesde forma agressiva. Essa é a alavanca mais importante. Cada classe adicionada ao escopo contribui com seu conjunto completo de mutantes para a execução, e o tempo de execução escala com o número total de mutantes gerados. Concentre-se na lógica de negócios (serviços, objetos de domínio, validadores) e exclua:
- Classes de dados, registros, DTOs
- Classes de configuração do Spring
- Código gerado (clientes Swagger, tipos Q do QueryDSL, mapeadores Mapstruct)
- Pontos de entrada da aplicação e classes de inicialização
Uso withHistorylocal. Ao executar o PIT repetidamente durante o desenvolvimento, withHistoryele reutiliza os resultados de classes inalteradas. A primeira execução é lenta. Execuções subsequentes em código inalterado são instantâneas.
Exclua testes lentos da execução de mutações. Se seu conjunto de testes incluir testes de integração ou @SpringBootTestslices lentos, o PIT tentará executá-los para cada mutante. Use `–fast-test` excludedTestClassesou ` targetTests–fast-test` para restringir o PIT apenas a testes unitários rápidos. Execute os testes de integração em uma etapa de CI separada.
Use o modo de simulação para depurar problemas de configuração. Se o PIT estiver apresentando comportamento inesperado (classes incorretas encontradas, testes não descobertos), adicione <dryRun>true</dryRun>as alterações necessárias à configuração. O modo de simulação coleta cobertura e gera mutantes sem executá-los nos testes. É muito mais rápido e revela erros de configuração sem a necessidade de aguardar a execução completa de mutações.
Conclusão
A cobertura de linhas é um requisito básico necessário. No entanto, não é um indicador suficiente da qualidade dos testes. O teste de mutação é o que preenche essa lacuna. Ele informa não apenas quais linhas seus testes abrangem, mas também quais alterações lógicas seus testes de fato detectariam.
O PIT é a maneira mais prática de adicionar testes de mutação a um projeto Java atualmente. Com o plugin JUnit 5 instalado e um perfil Maven encapsulando a configuração, você pode integrá-lo a um projeto Spring Boot existente em menos de uma hora, executá-lo em CI com um fluxo de trabalho do GitHub Actions e obter um relatório que indica exatamente onde fortalecer suas asserções.
Um caminho razoável para a adoção:
- Adicione o plugin com a dependência do JUnit 5. Execute uma vez localmente. Estabeleça uma pontuação de mutação de referência.
- O escopo se restringe a pacotes de lógica de negócios. Exclui DTOs, configurações e código gerado.
- Defina um limite igual ou ligeiramente inferior à linha de base. Confirme.
- Adicione verificações específicas para mutantes sobreviventes em classes críticas. Aumente o limite conforme você aprimora o processo.
- Adicione o fluxo de trabalho do GitHub Actions. Arquive relatórios. Controle os PRs com base no limite estabelecido.
- Utilize
withHistorylocalmente para manter o ciclo de feedback rápido durante o desenvolvimento.
O objetivo não é 100%. O objetivo é ter confiança de que os testes que você realizou detectarão os erros que importam.




