Glossário · Fundamentos de testes
Teste de mutação
Em inglês: Mutation testing
Também chamado de: análise de mutação, mutation score, testes de mutação, score de mutação
Definição
Técnica que injeta pequenos bugs deliberados (mutantes) no código e roda os testes de novo para medir se a suíte realmente detecta falhas.
O teste de mutação (mutation testing) testa os seus testes. Uma ferramenta cria muitas cópias do código, cada uma com uma pequena alteração, como >= virando >, + virando - ou o corpo de um método substituído por return null. Cada cópia alterada é um mutante. A suíte de testes roda contra cada mutante. Se pelo menos um teste falha, o mutante é morto (killed). Se todos os testes continuam passando, o mutante sobreviveu (survived), o que significa que os testes não perceberiam aquele bug.
Mutation score (score de mutação) = mutantes mortos / total de mutantes (excluindo os mutantes equivalentes).
Exemplo
boolean isAdult(int age) {
return age >= 18;
}
Testes: isAdult(30) é true, isAdult(10) é false. A cobertura de linhas e de branches é de 100%.
A ferramenta cria um mutante com age > 18. Os dois testes continuam passando, então o mutante sobrevive. Isso expõe um teste de valor-limite que está faltando: ninguém verificou isAdult(18). Adicioná-lo mata o mutante. A cobertura não conseguiria te mostrar isso; o teste de mutação consegue.
Ferramentas
- Java: PIT (pitest), com plugins para Maven e Gradle e suporte a JUnit 5.
- JavaScript/TypeScript: StrykerJS, que também tem versões para .NET e Scala.
- Python: mutmut e Cosmic Ray.
Limites práticos
- Custo. Cada mutante é uma execução de testes. As ferramentas reduzem isso rodando apenas os testes que cobrem a linha mutada e executando de forma incremental sobre o código alterado, mas ainda assim é muito mais lento que uma execução normal. A maioria dos times roda à noite ou sobre os arquivos alterados em um pull request, e não a cada commit para a base de código inteira.
- Mutantes equivalentes. Algumas alterações não mudam o comportamento, por exemplo trocar
i < npori != nem um loop no qualisempre cresce de um em um a partir de um valor abaixo den. Nenhum teste consegue matá-los, então eles precisam ser revisados e excluídos manualmente. - Funciona melhor no nível de unidade. O teste de mutação foi pensado para testes unitários rápidos e determinísticos. Rodá-lo contra testes de UI lentos raramente é viável.
Use os mutantes sobreviventes como uma lista de tarefas de assertions que estão faltando, especialmente na lógica de negócio, em vez de perseguir um score perfeito.