Simple Science

Ciência de ponta explicada de forma simples

# Informática# Engenharia de software

Geração de Testes Eficiente com Sementes de Contradição

Um método mais rápido para gerar suítes de teste completas no desenvolvimento de software.

― 9 min ler


Método Rápido de GeraçãoMétodo Rápido de Geraçãode Testessoftware rápido.Uma nova abordagem para gerar testes de
Índice

No desenvolvimento de software, garantir que um programa funcione corretamente depois de mudanças é fundamental. Um conjunto de testes chamado suíte de testes de regressão ajuda a garantir que funções antigas não quebrem quando novas alterações são feitas. Para ser realmente eficaz, esses testes devem cobrir todas as partes do programa. No entanto, criar esses testes manualmente é muito demorado, e as ferramentas automatizadas existentes podem ser lentas e ineficientes.

O método "Seeding Contradiction" oferece uma solução mais rápida para gerar um conjunto completo de testes. Esse método envolve a introdução de instruções falsas ou incorretas em cada parte básica do programa. Isso permite que um tipo especial de verificador de programa encontre erros em todos os ramos do código e crie testes a partir desses erros. O processo é rápido, não requer a execução do código e alcança uma excelente Cobertura.

Importância do Teste

Testar é uma parte crucial da engenharia de software. Todo programa que vai para o ar deve ter um conjunto de testes prontos. Esses testes servem para confirmar que, após cada mudança, o software ainda funciona como esperado. O objetivo é evitar regressões, onde as alterações criam novos problemas em áreas que antes funcionavam bem.

Criar uma suíte de testes de regressão com alta cobertura é desafiador. Embora existam várias ferramentas, como RANDOOP, Pex e AutoTest, muitas delas exigem a execução do código diversas vezes, o que pode ser lento. O método Seeding Contradiction, por outro lado, consegue uma cobertura total rapidamente e sem rodar o código, tornando-se uma escolha mais eficiente.

A Ideia Principal por trás do Seeding Contradiction

Seeding Contradiction aproveita os verificadores de programa modernos. Esses verificadores são projetados para verificar se um programa está correto. Normalmente, a função de um verificador é provar a correção, mostrando que não existem erros. No entanto, ao introduzir erros de propósito, podemos mudar o foco para gerar testes a partir dos erros encontrados.

Essa ideia pode ser comparada a encontrar falhas em uma prova para criar casos de teste. Em trabalhos anteriores, pesquisadores usaram com sucesso esse conceito de várias maneiras. Seeding Contradiction se baseia nessa abordagem, visando criar um conjunto completo de testes ao introduzir erros em ramos específicos do programa. Para cada versão alterada do programa, o verificador gera testes que exercitam cada ramo. O resultado é uma suíte de testes que geralmente cobre todos os ramos.

Como o Método Funciona

Para ilustrar esse método, vamos considerar um programa simples com uma condição:

simple (a: INTEGER)
if a > 0 then 
  x := 1 
else 
  x := 2 
end

Nesse programa, x é definido com base no valor de a. Para garantir que geramos testes para ambos os resultados possíveis (quando a é maior que zero e quando não é), podemos usar o método Seeding Contradiction.

Tradicionalmente, alcançar isso exigiria a execução do código. No entanto, Seeding Contradiction funciona de forma diferente. Ele assume que há um conjunto de ferramentas disponíveis para verificar a correção do programa. O processo depende de uma estrutura de verificação específica projetada para garantir que o programa atenda ao seu comportamento pretendido.

Ao inserir uma instrução defeituosa, como uma verificação que o programa espera que seja verdadeira, mas que na verdade é falsa, podemos enganar o verificador de programa para gerar um caso de teste. Para o programa acima, podemos modificá-lo para ficar assim:

simple (a: INTEGER)
if a > 0 then
  check False end
  x := 1
else
  x := 2
end

Com a instrução adicionada check False end, o verificador falhará em provar a correção. O resultado é um contraexemplo que mostra como o primeiro ramo pode ser exercitado em um teste. Ao repetir esse processo para cada ramo, geramos testes que cobrem todo o programa.

Gerando Testes para Todos os Ramos

Para fazer o método funcionar para todos os ramos, um novo conceito chamado "Programas Múltiplos Semeados" (MSP) pode ser usado. Nessa abordagem, criamos uma versão do programa para cada bloco ou seção individual onde uma decisão é tomada. Depois de fazer as mudanças, o verificador é executado para cada versão modificada do programa, garantindo que cada ramo seja testado.

No entanto, gerar versões separadas do programa para cada ramo pode se tornar impraticável, levando a um grande número de variantes. Em vez disso, é usada a abordagem "Programa Único Repetidamente Semeado" (RSSP). Dessa forma, conseguimos criar uma única versão do programa que contém todas as falhas possíveis. Isso significa que ainda podemos produzir múltiplos contraexemplos a partir de apenas uma execução, o que é muito mais eficiente.

Por exemplo, se quisermos testar uma seção de código com várias condições, poderíamos configurá-lo assim:

simple (a: INTEGER)
if a > 0 then
  check False end
  x := 1
check False end
x := 2
if a > a then
  check False end
  x := 3
check False end
x := 4

Ao aplicar o método aqui, conseguimos garantir que todos os caminhos do programa original sejam testados sem gerar inúmeras versões separadas. O verificador produzirá diferentes contraexemplos para cada verificação defeituosa inserida, proporcionando uma cobertura abrangente de testes para todos os ramos.

Abordando a Inutilidade Semeada

Um problema na criação de testes vem da 'Inutilidade Semeada'. Esse termo se refere a situações em que alguns ramos podem não ser acessíveis devido à maneira como as checagens estão configuradas, levando a testes incompletos. Para resolver isso, podemos introduzir uma variável local que ajuda a determinar qual seção do código deve ser executada.

A lógica é que, antes de qualquer bloco, introduzimos uma verificação que depende do valor de uma variável. Nesse cenário, uma variável bn pode receber um valor que indica qual parte do código checar. Isso significa que todos os ramos são acessíveis, e o verificador pode gerar testes que cobrem todos os caminhos possíveis.

Por exemplo, usando o exemplo anterior, podemos modificá-lo para incluir:

bn := "Valor escolhido de forma não determinística entre 0 e N"
if a > 0 then
  if bn == 1 then 
    check False end 
  x := 1
  if bn == 2 then 
    check False end 
  x := 2
if a > a then
  if bn == 3 then 
    check False end 
  x := 3
  if bn == 4 then 
    check False end 
  x := 4

Qualquer teste gerado então verifica caminhos válidos, mesmo que alguns blocos não fossem acessíveis no código original. Essa técnica é chamada de Semeadura Condicional. Ela facilita a geração de testes que cobrem de forma abrangente todos os ramos.

Provando Correção

O objetivo de gerar testes é garantir alta cobertura. A estratégia de Seeding Contradiction visa uma cobertura exaustiva, significando que cada parte acessível do programa é testada. Para garantir isso, várias suposições sobre o verificador de programa precisam estar em vigor:

  • O verificador deve ser sólido em relação à acessibilidade: se uma seção pode ser acessada, ela será reconhecida como acessível.
  • O verificador deve ser sólido em relação à correção: se ele diz que uma parte está correta, ela realmente está.
  • O verificador deve produzir contraexemplos para todas as seções incorretas.

Dadas essas condições, pode-se mostrar que o método Seeding Contradiction produz uma suíte de testes correta e abrangente. Isso significa que, se o programa original for correto, os testes gerados também garantirão que todos os caminhos sejam testados.

Aplicações Práticas

Para verificar se a abordagem Seeding Contradiction funciona bem, ela foi implementada como uma opção em uma ferramenta de verificação de programas chamada AutoProof. Essa ferramenta pode gerar automaticamente testes a partir de falhas de prova, facilitando muito o processo de teste.

Em testes práticos, o método mostrou produzir resultados rápidos enquanto mantém alta cobertura. Foi implementado especificamente para a linguagem de programação Eiffel, mas poderia potencialmente se adaptar a outros ambientes de programação também.

Avaliação de Desempenho

Um estudo comparando o método Seeding Contradiction com ferramentas de geração de teste existentes, como IntelliTest e AutoTest, mostrou resultados promissores. Foi avaliado com base na cobertura, velocidade e no tamanho da suíte de testes gerada.

Os resultados destacaram que o Seeding Contradiction consistentemente alcançou cerca de 100% de cobertura em vários exemplos, enquanto usava muito menos tempo e gerava suítes de testes menores.

  • O Seeding Contradiction completou a geração de testes em um tempo médio de menos de um segundo.
  • O IntelliTest levou significativamente mais tempo, com uma média de cerca de 27 segundos.
  • O AutoTest, com sua abordagem aleatória, exigiu ainda mais tempo, às vezes ultrapassando 259 segundos.

Em termos do número de testes gerados, o Seeding Contradiction produziu o menor número de testes, correspondendo de perto ao número de blocos passíveis de cobertura.

Limitações e Direções Futuras

Embora o método Seeding Contradiction mostre grande promessa, ele tem algumas limitações. Estas incluem:

  • O método pode ter dificuldades com programas que exigem cálculos complexos, já que o verificador subjacente pode não lidar bem com eles.
  • Ele não suporta atualmente recursos avançados da linguagem de programação, como generics.

Trabalhos futuros se concentrarão em superar essas limitações, explorando programas maiores, permitindo o processamento de classes inteiras e aprimorando a geração de testes para cobrir lacunas onde os testes existentes podem falhar.

Conclusão

O método Seeding Contradiction oferece uma maneira empolgante e eficiente de gerar suítes de testes abrangentes. Com foco em desempenho rápido e alta cobertura, representa um avanço significativo no campo dos testes de software. À medida que a pesquisa contínua aprimora o método e aborda limitações existentes, ele pode beneficiar desenvolvedores de software em todo lugar, garantindo que seus programas permaneçam robustos e livres de erros após cada alteração.

Mais de autores

Artigos semelhantes