Teste de mutação - Mutation testing

Testes de mutação (ou análise de mutação ou mutação programa ) é usado para projetar novos testes de software e avaliar a qualidade dos testes de software existentes. O teste de mutação envolve a modificação de um programa em pequenas maneiras. Cada versão mutada é chamada de mutante e os testes detectam e rejeitam mutantes, fazendo com que o comportamento da versão original seja diferente do mutante. Isso é chamado de matar o mutante. Os conjuntos de testes são medidos pela porcentagem de mutantes que eles matam. Novos testes podem ser projetados para matar mutantes adicionais. Os mutantes são baseados em operadores de mutação bem definidos que imitam erros de programação típicos (como usar o operador ou nome de variável errado) ou forçam a criação de testes valiosos (como dividir cada expressão por zero). O objetivo é ajudar o testador a desenvolver testes eficazes ou localizar pontos fracos nos dados de teste usados ​​para o programa ou em seções do código que raramente ou nunca são acessadas durante a execução . O teste de mutação é uma forma de teste de caixa branca .

Introdução

A maior parte deste artigo é sobre "mutação de programa", na qual o programa é modificado. Uma definição mais geral de análise de mutação é usar regras bem definidas definidas em estruturas sintáticas para fazer mudanças sistemáticas em artefatos de software. A análise de mutação foi aplicada a outros problemas, mas geralmente é aplicada a testes. Portanto, o teste de mutação é definido como o uso de análise de mutação para projetar novos testes de software ou para avaliar os testes de software existentes. Assim, a análise e o teste de mutação podem ser aplicados a modelos de design, especificações, bancos de dados, testes, XML e outros tipos de artefatos de software, embora a mutação de programa seja a mais comum.

Visão geral

Testes podem ser criados para verificar a exatidão da implementação de um determinado sistema de software, mas a criação de testes ainda coloca a questão se os testes estão corretos e cobrem suficientemente os requisitos que originaram a implementação. (Este problema tecnológico é em si uma instância de um problema filosófico mais profundo chamado " Quis custodiet ipsos custodes? " ["Quem guardará os guardas?"].) A ideia é que, se um mutante for introduzido sem ser detectado pelo conjunto de testes, isso indica que o código que sofreu mutação nunca foi executado (código morto) ou que o conjunto de testes foi incapaz de localizar as falhas representadas pelo mutante.

Para que isso funcione em qualquer escala, geralmente é introduzido um grande número de mutantes, levando à compilação e execução de um número extremamente grande de cópias do programa. Esse problema de despesas com testes de mutação reduziu seu uso prático como método de teste de software. No entanto, o aumento do uso de linguagens de programação orientadas a objetos e estruturas de teste de unidade levou à criação de ferramentas de teste de mutação que testam partes individuais de um aplicativo.

Metas

Os objetivos do teste de mutação são múltiplos:

  • identificar pedaços de código fracamente testados (aqueles para os quais os mutantes não são mortos)
  • identificar testes fracos (aqueles que nunca matam mutantes)
  • calcular a pontuação de mutação, a pontuação de mutação é o número de mutantes mortos / número total de mutantes.
  • aprender sobre propagação de erro e infecção de estado no programa

História

O teste de mutação foi originalmente proposto por Richard Lipton como um estudante em 1971 e primeiro desenvolvido e publicado por DeMillo, Lipton e Sayward. A primeira implementação de uma ferramenta de teste de mutação foi feita por Timothy Budd como parte de seu trabalho de doutorado (intitulado Mutation Analysis ) em 1980 da Universidade de Yale .

Recentemente, com a disponibilidade de grande poder de computação, houve um ressurgimento da análise de mutação dentro da comunidade de ciência da computação e trabalho foi feito para definir métodos de aplicação de teste de mutação para linguagens de programação orientadas a objetos e linguagens não procedimentais , como XML , SMV e máquinas de estado finito .

Em 2004, uma empresa chamada Certess Inc. (agora parte da Synopsys ) estendeu muitos dos princípios ao domínio de verificação de hardware. Enquanto a análise de mutação espera apenas detectar uma diferença na saída produzida, a Certess estende isso verificando se um verificador no testbench realmente detectará a diferença. Esta extensão significa que todos os três estágios de verificação, a saber: ativação, propagação e detecção, são avaliados. Eles chamaram isso de qualificação funcional.

A difusão pode ser considerada um caso especial de teste de mutação. No fuzzing, as mensagens ou dados trocados dentro das interfaces de comunicação (tanto dentro quanto entre as instâncias do software) são transformados para detectar falhas ou diferenças no processamento dos dados. Codenomicon (2001) e Mu Dynamics (2005) desenvolveram conceitos de fuzzing para uma plataforma de teste de mutação totalmente stateful, completa com monitores para exercitar completamente as implementações de protocolo.

Visão geral do teste de mutação

O teste de mutação é baseado em duas hipóteses. A primeira é a hipótese do programador competente . Esta hipótese afirma que a maioria das falhas de software introduzidas por programadores experientes são devidas a pequenos erros sintáticos. A segunda hipótese é chamada de efeito de acoplamento . O efeito de acoplamento afirma que falhas simples podem se espalhar ou se acoplar para formar outras falhas emergentes.

Falhas sutis e importantes também são reveladas por mutantes de ordem superior, que suportam ainda mais o efeito de acoplamento. Mutantes de ordem superior são ativados pela criação de mutantes com mais de uma mutação.

O teste de mutação é feito selecionando um conjunto de operadores de mutação e aplicando-os ao programa de origem, um de cada vez, para cada parte aplicável do código-fonte. O resultado da aplicação de um operador de mutação ao programa é chamado de mutante . Se o conjunto de testes é capaz de detectar a mudança (ou seja, um dos testes falha), então o mutante é considerado morto .

Por exemplo, considere o seguinte fragmento de código C ++:

if (a && b) {
    c = 1;
} else {
    c = 0;
}

O operador condição mutação substituiria && com || e produzir o mutante seguinte:

if (a || b) {
    c = 1;
} else {
    c = 0;
}

Agora, para o teste matar esse mutante, as três condições a seguir devem ser atendidas:

  1. Um teste deve atingir a declaração mutada.
  2. Os dados de entrada de teste devem infectar o estado do programa, causando diferentes estados do programa para o mutante e o programa original. Por exemplo, um teste com a = 1 e b = 0 faria isso.
  3. O estado incorreto do programa (o valor de 'c') deve se propagar para a saída do programa e ser verificado pelo teste.

Essas condições são chamadas coletivamente de modelo RIP .

O teste de mutação fraca (ou cobertura de mutação fraca ) requer que apenas a primeira e a segunda condições sejam satisfeitas. O teste de mutação forte requer que todas as três condições sejam satisfeitas. A mutação forte é mais poderosa, pois garante que o conjunto de testes possa realmente detectar os problemas. A mutação fraca está intimamente relacionada aos métodos de cobertura de código . Exige muito menos poder de computação para garantir que o conjunto de testes satisfaça o teste de mutação fraco do que o teste de mutação forte.

No entanto, há casos em que não é possível encontrar um caso de teste que possa matar esse mutante. O programa resultante é comportamentalmente equivalente ao original. Esses mutantes são chamados de mutantes equivalentes .

A detecção de mutantes equivalentes é um dos maiores obstáculos para o uso prático de testes de mutação. O esforço necessário para verificar se os mutantes são equivalentes ou não pode ser muito alto, mesmo para programas pequenos. Uma revisão sistemática da literatura de uma ampla gama de abordagens para superar o Problema Mutante Equivalente identificou 17 técnicas relevantes (em 22 artigos) e três categorias de técnicas: detecção (DEM); sugerindo (SEM); e evitando a geração de mutantes equivalentes (AEMG). O experimento indicou que a mutação de ordem superior em geral e a estratégia JudyDiffOp em particular fornecem uma abordagem promissora para o problema mutante equivalente.

Operadores de mutação

Muitos operadores de mutação foram explorados por pesquisadores. Aqui estão alguns exemplos de operadores de mutação para linguagens imperativas:

  • Exclusão de extrato
  • Duplicação ou inserção de extrato, por exemplo goto fail;
  • Substituição de subexpressões booleanas por verdadeiro e falso
  • Substituição de algumas operações aritméticas com os outros, por exemplo, + com * , - com /
  • Substituição de algumas relações booleanas com outras, por exemplo , > com >= , == e <=
  • Substituição de variáveis ​​por outras do mesmo escopo (os tipos de variáveis ​​devem ser compatíveis)
  • Remova o corpo do método, implementado em Pitest

Esses operadores de mutação também são chamados de operadores de mutação tradicionais. Existem também operadores de mutação para linguagens orientadas a objetos, para construções simultâneas, objetos complexos como contêineres, etc. Os operadores de contêineres são chamados de operadores de mutação em nível de classe . Por exemplo, a ferramenta muJava oferece vários operadores de mutação em nível de classe, como Alteração do Modificador de Acesso, Inserção de Operador de Tipo e Exclusão de Operador de Tipo. Os operadores de mutação também foram desenvolvidos para realizar testes de vulnerabilidade de segurança de programas.

Veja também

Referências