Seus testes estão mentindo, e você nem sabe
\n\nImagine a situação: você escreveu testes unitários, o code coverage (cobertura de código) é de 95%. O chefe está satisfeito, o pipeline CI/CD está verde. Mas na sexta-feira à noite, um bug aparece em produção. Familiar?
\n\nO problema é que a cobertura de código clássica é uma ilusão. Ela mostra quais linhas de código foram executadas, mas não responde à pergunta principal: os testes realmente verificam a lógica? É aqui que entra o mutation testing — uma técnica que "quebra" impiedosamente seu código para verificar o quão bons são seus testes.
\n\nNeste artigo, vamos explorar o que é teste de mutação em Java, como ele funciona e mostrar exemplos reais usando a ferramenta PIT (Pitest). Você aprenderá como transformar seus testes de "um item no relatório" em uma proteção real contra bugs.
\n\nO que é mutation testing e por que é importante?
\n\nMutation testing (ou teste de mutação) é um método de avaliação da qualidade dos testes, no qual pequenas alterações deliberadamente incorretas (chamadas de mutações) são introduzidas no código-fonte. Em seguida, seus testes são executados. Se pelo menos um teste falhar — o mutante foi "morto", o teste funcionou. Se os testes passarem — o mutante "sobreviveu", e isso é um sinal de alerta.
\n\nUma metáfora simples: você verifica o quão bem seu alarme detecta ladrões. Você coloca um "ladrão" (mutante) e observa se o alarme (teste) dispara. Se o ladrão sai tranquilamente, o alarme não serve para nada.
\n\nPor que o code coverage é um indicador ruim?
\n\nConsidere este código:
\n\npublic boolean isAdult(int age) {\n if (age >= 18) {\n return true;\n }\n return false;\n}\n\nE o teste:
\n\n@Test\nvoid testIsAdult() {\n assertTrue(isAdult(18));\n}\n\nA cobertura de código mostrará 100% (ambos os ramos do if-else foram executados?). Não! Apenas o ramo true foi executado. Mas o teste está verde. Agora imagine que alguém acidentalmente muda a condição para age > 21. O teste ainda passará, porque 18 > 21 é false, e assertTrue espera true. O bug irá para produção.
O teste de mutação pegaria essa situação instantaneamente, criando um mutante que altera >= para > ou inverte a condição.
Como funciona o mutation testing: teoria simplificada
\n\nO processo consiste em três etapas:
\n\n- \n
- Geração de mutantes. A ferramenta (por exemplo, PIT) cria várias versões do seu código com pequenas alterações: troca operadores, remove chamadas de métodos, inverte condições. \n
- Execução dos testes. Para cada mutante, seus testes unitários são executados. \n
- Análise dos resultados. Se o teste falhar — o mutante foi morto. Se passar — está vivo. A porcentagem de mutantes mortos é chamada de mutation score (pontuação de mutação). \n
Um bom indicador: 80-90% de mutantes mortos. Qualquer valor abaixo disso é motivo para reescrever os testes.
\n\nExemplo prático em Java com PIT
\n\nVamos usar um exemplo real. Suponha que temos um serviço para calcular desconto:
\n\npublic class DiscountService {\n public double calculateDiscount(double amount, boolean isPremium) {\n double discount = 0.0;\n\n if (amount > 100) {\n discount = 0.1; // 10% de desconto\n }\n\n if (isPremium) {\n discount += 0.05; // adicional 5%\n }\n\n // O desconto não pode ser maior que 30%\n if (discount > 0.3) {\n discount = 0.3;\n }\n\n return discount;\n }\n}\n\nVamos escrever um teste ingênuo:
\n\nimport org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\n\nclass DiscountServiceTest {\n\n private final DiscountService service = new