Por que a cobertura de código é uma armadilha e os testes de mutação são a salvação
\n\nImagine a situação: seu projeto em Kotlin tem 95% de cobertura de testes. O CI está verde, o gerente está feliz, o código vai para produção. E uma semana depois — um bug em produção. Familiar? Muito provavelmente, seus testes apenas são executados, mas não verificam a lógica. É aqui que entram em cena os testes de mutação (mutation testing) — uma metodologia que não permite que os testes "preguiçem".
\n\nNeste artigo, vamos explorar o que são testes de mutação em Kotlin, como funcionam e como implementá-los em seu projeto usando a ferramenta popular Pitest. Você descobrirá por que 100% de cobertura de código não é garantia de qualidade e como os testes de mutação ajudam a escrever testes verdadeiramente confiáveis.
\n\nO que são testes de mutação? Explicando com gatos
\n\nSuponha que você escreveu uma função que determina se um gato pode passar por uma porta:
\n\nfun canCatFitThroughDoor(catWidth: Int, doorWidth: Int): Boolean {\n return catWidth <= doorWidth\n}\n\nVocê escreveu um teste:
\n\n@Test\nfun `gato de largura 30 passa pela porta 40`() {\n assertTrue(canCatFitThroughDoor(30, 40))\n}\n\nCobertura — 100%. Mas e se alguém acidentalmente mudar <= para <? O teste ainda passará! Os testes de mutação introduzem automaticamente essas "pequenas quebras" (mutações) no código e verificam se pelo menos um teste falha. Se o teste não falhar, seus testes não estão detectando esse erro. Isso é o que chamamos de testes de mutação em Kotlin.
Como funcionam os testes de mutação: de operadores a mutantes "sobreviventes"
\n\nA ferramenta (por exemplo, Pitest para Kotlin) cria mutantes — versões do seu código com uma única alteração. Aqui estão as mutações típicas:
\n\n- \n
- Alteração de operadores de comparação:
>→>=,==→!=\n - Inversão de condições:
if (a > b)→if (a <= b)\n - Remoção de chamadas de métodos:
someMethod()→ vazio \n - Substituição de valores de retorno:
true→false\n - Alteração de números:
10→11ou0\n
Após criar os mutantes, todos os seus testes são executados. Os resultados são divididos em três categorias:
\n\n- \n
- Mutantes mortos (Killed) — os testes falharam, a mutação foi detectada. Ótimo! \n
- Mutantes sobreviventes (Survived) — os testes passaram, embora o código tenha sido alterado. Um sinal de alerta. \n
- Cobertos, mas não mortos (Covered but not killed) — os testes foram executados, mas não perceberam a alteração. \n
Seu objetivo é ter o menor número possível de mutantes sobreviventes (idealmente 0). Isso é o que chamamos de testes de mutação de código Kotlin em ação.
\n\nPitest: a principal ferramenta para Kotlin
\n\nPara linguagens JVM (Java, Kotlin), o padrão de facto é o Pitest (PIT Mutation Testing). Ele suporta Kotlin através do plugin pitest-kotlin. Veja como configurá-lo no Gradle:
plugins {\n id("info.solidsoft.pitest") version "1.15.0"\n}\n\npitest {\n targetClasses.set(listOf("com.myapp.*")) // quais classes mutar\n targetTests.set(listOf("com.myapp.*")) // quais testes executar\n threads.set(4) // threads paralelas\n outputFormats.set(listOf("XML", "HTML")) // relatórios\n mutationThreshold.set(80) // percentual mínimo de mortos\n timeoutConstInMillis.set(10000) // timeout por teste\n}\n\ndependencies {\n testImplementation("org.pitest:pitest-kotlin:1.15.0")\n}\n\nExecução com um único comando:
\n\n./gradlew pitest\n\nApós a execução, em build/reports/pites