O teste de mutação é uma técnica para avaliar a qualidade dos testes de software e projetar novos casos de verificação. Neste artigo, vamos explicar o que são testes de mutação, como funcionam, quais são seus benefícios e desafios, além de como aplicá-los na prática.
O que é teste de mutação?
Teste de mutação é baseado na ideia de que se um programa funciona corretamente, então qualquer alteração no seu código-fonte deve causar uma falha em pelo menos um dos testes.
Portanto, o teste de mutação consiste em introduzir pequenas mudanças no código-fonte de um programa e verificar se os testes existentes conseguem detectar essas mudanças, ou seja, se eles falham ao executar o código mutado.
Um mutante é uma versão modificada do programa original que contém uma ou mais alterações sintáticas no código-fonte, essas alterações são chamadas de operadores de mutação, cada um deles gera um ou mais mutantes a partir do programa original.
Para entender melhor, os operadores de mutação são regras que definem como modificar o código, realizando a troca de um operador aritmético, a inversão de uma condição lógica, a remoção de uma chamada de método ou a inserção de uma instrução extra. Por exemplo, um operador de mutação pode trocar um operador aritmético por outro (+ por -), ou inverter uma condição lógica (== por !=).
Um mutante é considerado morto se algum teste falha ao executar o código mutado. Isso significa que o teste foi capaz de identificar a alteração no código e que o original é necessário para o funcionamento correto do programa.
Por outro lado, um mutante é considerado vivo se nenhum teste falha ao executar o código mutado. Isso significa que o teste não foi capaz de identificar a alteração e que o código original é desnecessário ou redundante para o funcionamento correto do programa.
O objetivo dos testes de mutação é gerar o maior número possível de mutantes mortos, pois isso indica que os testes existentes são efetivos em detectar defeitos no código-fonte. A taxa de mutantes mortos em relação ao total de mutantes gerados é chamada de índice de mutação (ou mutation score) e é usada como uma medida da qualidade dos testes.
Quais são os benefícios dos testes de mutação?
Os testes de mutação trazem vários benefícios para o desenvolvimento e a manutenção de software, tais como:
- Aumentam a confiança na qualidade dos testes existentes, pois eles são capazes de detectar alterações sutis no código-fonte que podem introduzir defeitos;
- Auxiliam na criação de novos casos de teste, pois eles revelam as partes do código-fonte que não são cobertas pelos testes existentes ou que são cobertas por testes fracos ou irrelevantes;
- Contribuem para a melhoria do código-fonte, pois eles identificam o código desnecessário e redundante que pode ser removido ou simplificado sem afetar o comportamento do programa;
- Estimulam as boas práticas de programação, pois incentivam as pessoas desenvolvedoras a escrever códigos simples, claros e legíveis que sejam fáceis de testar e modificar.
Falando nesse último item, temos uma live do Zup Tech Hour que fala exatamente sobre boas práticas de programação. Vale assistir!
Como funcionam os testes de mutação?
Os testes de mutação são uma forma de teste de caixa-branca, pois eles exigem acesso ao código-fonte do programa. Eles também são considerados um tipo de teste estrutural, pois se baseiam na estrutura interna do código para gerar os mutantes.
Para realizar os testes de mutação, é necessário usar uma ferramenta ou framework específico que seja capaz de gerar os mutantes a partir do código original e executar os testes sobre eles.
Existem diversas ferramentas disponíveis para diferentes linguagens e ambientes de desenvolvimento, como: Pitest (para Java), Stryker (para JavaScript), MutMut (para Python).
Os testes de mutação seguem os seguintes passos:
1. Seleciona um programa e um conjunto de testes para serem analisados;
2. Aplica operadores de mutação no código-fonte do programa para gerar diferentes versões mutadas do programa. Os operadores de mutação são regras que definem como modificar o código-fonte, por exemplo, trocar um operador aritmético por outro, inverter uma condição lógica, remover uma chamada de método, etc.;
3. Executa os testes existentes em cada versão mutada do programa e verifica se eles falham ou não;
4. Calcula o índice de mutação como a porcentagem de mutantes mortos em relação ao total de mutantes gerados;
5. Analisa os resultados e identifica possíveis melhorias nos testes ou no código-fonte.
Um exemplo prático em Java
Dado que tenho uma classe com a seguinte função:
Classe Message:
public class Message {
public String message(int a) {
String result;
if (a >= 0 && a <= 10) {
result = "YES";
} else {
result = "NO";
}
return result;
}
}
Abaixo a classe de teste da função mencionada acima:
Classe: MessageTest
public class MessageTest {
@Test
public void messageOk1() {
Message message = new Message();
String result = message.message(5);
assertEquals("YES", result);
}
@Test
public void messageOk2() {
Message message = new Message();
String result = message.message(-5);
assertEquals("NO", result);
}
Neste teste unitário, estou validando se o resultado esperado será “YES” ou “NO”. O teste passa com sucesso e a cobertura é de 100%. Porém, ao executar o teste de mutação, o sistema identifica que há mutações que poderiam quebrar o teste, indicando uma cobertura de apenas 60%.
No relatório do PiTest, você terá a informação conforme a imagem abaixo:
O relatório do Pitest destacou a necessidade de incluirmos testes de limites para alcançarmos uma cobertura de 100%. Para solucionar esse problema, podemos adicionar novos testes que garantam a completa cobertura da função em questão.
Classe: MessageTest
public class MessageTest {
@Test
public void messageOk1() {
Message message = new Message();
String result = message.message(5);
assertEquals("YES", result);
}
@Test
public void messageOk2() {
Message message = new Message();
String result = message.message(-5);
assertEquals("NO", result);
}
@Test
public void messageLimit1() {
Message message = new Message();
String result = message.message(0);
assertEquals("YES", result);
}
@Test
public void messageLimit2() {
Message message = new Message();
String result = message.message(10);
assertEquals("YES", result);
}
@Test
public void messageUnderZero() {
Message message = new Message();
String result = message.message(-1);
assertEquals("NO", result);
}
@Test
public void messageAboveten() {
Message message = new Message();
String result = message.message(11);
assertEquals("NO", result);
}
}
Após incluir os testes necessários e executá-los novamente pelo Pitest, obtivemos a cobertura de 100%. O relatório confirma que agora todas as mutações são detectadas e o código está mais confiável. Segue abaixo o resultado detalhado do relatório para conferência:
Você pode baixar e executar os testes desse exemplo no repositório no GitHub.
Quais são os desafios dos testes de mutação?
Os testes de mutação também apresentam alguns desafios e limitações, que devem ser considerados antes de escolher essa opção. Alguns desses desafios são:
Tempo
Demandam muito tempo e recursos computacionais, pois eles envolvem a geração e a execução de um grande número de versões mutadas do programa. Isso pode tornar os testes de mutação inviáveis para programas grandes ou complexos ou para ambientes com restrições de tempo ou orçamento.
Mutantes equivalentes
O problema dos mutantes equivalentes. Mutantes equivalentes são aqueles que, apesar de terem uma alteração no código-fonte, não mudam o comportamento do programa original. Isso significa que eles não podem ser detectados pelos testes de mutação, pois produzem os mesmos resultados que o programa original para qualquer entrada.
Os mutantes equivalentes são um problema para os testes de mutação, pois reduzem a efetividade da técnica e aumentam o esforço necessário para analisar os mutantes. Existem algumas formas de evitar ou minimizar os mutantes equivalentes, como usar técnicas de análise estática ou dinâmica ou aplicar critérios de seleção de mutantes.
Seleção e priorização
A seleção e a priorização dos mutantes, que envolvem definir quais tipos de mutação serão aplicados e em quais partes do código, também são desafios. Essas decisões podem afetar a eficácia e a eficiência dos testes de mutação, e devem levar em conta fatores como a relevância, a frequência e a dificuldade das falhas potenciais.
Integração
Por último, a integração com outras técnicas e ferramentas de teste, que pode facilitar o uso dos testes de mutação no processo de desenvolvimento de software. Porém, nem todas as técnicas e ferramentas são compatíveis ou complementares aos testes de mutação, e podem exigir adaptações ou customizações.
Conclusão
O teste de mutação é uma técnica poderosa e promissora para melhorar a qualidade dos testes de software, mas também requer cuidado e planejamento para serem aplicados com sucesso.
Profissionais de desenvolvimento e de qualidade que desejam utilizar um teste de mutação devem estar cientes dos seus benefícios e desafios, e buscar as melhores práticas e ferramentas para superar as limitações.
Comente sua experiência com testes de mutação nos comentários e continue acompanhando os conteúdos do nosso blog.