Se todo caminho de um programa for testado, isso garante a localização de todos os bugs?
Se não, por que não? Como você pode passar por todas as combinações possíveis de fluxo de programa e não encontrar o problema, se houver algum?
Hesito em sugerir que "todos os bugs" possam ser encontrados, mas talvez seja porque a cobertura do caminho não é prática (pois é combinatória), portanto nunca é experimentada?
Nota: este artigo fornece um resumo rápido dos tipos de cobertura conforme penso sobre eles.
Respostas:
Não
Porque, mesmo que você teste todos os caminhos possíveis , ainda não os testou com todos os valores possíveis ou todas as combinações possíveis de valores . Por exemplo (pseudocódigo):
fonte
Além da resposta de Mason , também há outro problema: a cobertura não informa qual código foi testado, mas qual código foi executado .
Imagine que você tem um conjunto de testes com 100% de cobertura de caminho. Agora remova todas as asserções e execute o testinguite novamente. Voilà, o testsuite ainda tem 100% de cobertura de caminho, mas não testa absolutamente nada.
fonte
ON ERROR GOTO
também é um caminho, como é C doif(errno)
.Aqui está um exemplo mais simples para encerrar as coisas. Considere o seguinte algoritmo de classificação (em Java):
Agora, vamos testar:
Agora, considere que (A) essa chamada específica
sort
retorna o resultado correto, (B) todos os caminhos de código foram cobertos por este teste.Mas, obviamente, o programa não é realmente de ordem.
Daqui resulta que a cobertura de todos os caminhos de código não é suficiente para garantir que o programa não tenha bugs.
fonte
Considere a
abs
função, que retorna o valor absoluto de um número. Aqui está um teste (Python, imagine alguma estrutura de teste):Esta implementação está correta, mas obtém apenas 60% de cobertura de código:
Esta implementação está incorreta, mas obtém 100% de cobertura do código:
fonte
def abs(x): if x == -3: return 3 else: return 0
Você pode se livrar daelse: return 0
parte e obter 100% de cobertura, mas a função seria essencialmente inútil, mesmo que passe no teste de unidade.Mais uma adição à resposta de Mason , o comportamento de um programa pode depender do ambiente de tempo de execução.
O código a seguir contém um Use-After-Free:
Esse código é Comportamento indefinido, dependendo da configuração (release | debug), SO e compilador, resultará em comportamentos diferentes. Não apenas a cobertura do caminho não garante que você encontrará o UAF, mas seu conjunto de testes normalmente não cobre os vários comportamentos possíveis do UAF que dependem da configuração.
Em outra nota, mesmo se a cobertura do caminho garantir a localização de todos os bugs, é improvável que isso possa ser alcançado na prática em qualquer programa. Considere o seguinte:
Se sua suíte de testes pode gerar todos os caminhos para isso, parabéns por ser um criptógrafo.
fonte
cryptohash
, é um pouco difícil dizer o que é "suficientemente pequeno". Talvez demore dois dias para concluir em um supercalculador. Mas sim,int
pode ser um poucoshort
.Está claro nas outras respostas que 100% de cobertura de código em testes não significa 100% de correção de código, ou mesmo que todos os bugs que poderiam ser encontrados pelo teste serão encontrados (não importa os bugs que nenhum teste poderia detectar).
Outra maneira de responder a essa pergunta é a prática:
Existem, no mundo real, e de fato no seu próprio computador, muitos softwares que são desenvolvidos usando um conjunto de testes que oferecem 100% de cobertura e que ainda possuem bugs, incluindo bugs que seriam identificados por testes melhores.
Uma questão implícita, portanto, é:
As ferramentas de cobertura de código ajudam a identificar as áreas que uma pessoa esqueceu de testar. Isso pode ser bom (o código está comprovadamente correto, mesmo sem teste), pode ser impossível resolver (por algum motivo um caminho não pode ser encontrado) ou pode ser o local de um grande bug fedorento agora ou após futuras modificações.
De certa forma, a verificação ortográfica é comparável: algo pode "passar" na verificação ortográfica e ser digitado incorretamente, de forma a corresponder a uma palavra no dicionário. Ou pode "falhar" porque as palavras corretas não estão no dicionário. Ou pode passar e ser totalmente sem sentido. A verificação ortográfica é uma ferramenta que ajuda a identificar lugares que você pode ter perdido na leitura de provas, mas assim como não pode garantir a leitura completa e correta, a cobertura do código não pode garantir testes completos e corretos.
E, é claro, a maneira incorreta de usar a verificação ortográfica é famosa por acompanhar todas as sugestões que sugerimos, para que a coisa oculta se torne pior do que se a ovelha deixasse um empréstimo.
Com a cobertura do código, pode ser tentador, especialmente se você tiver 98% quase perfeito, preencher os casos para que os caminhos restantes sejam atingidos.
Isso é o equivalente a corrigir com costura ortográfica que são todas as palavras clima ou nó, são todas as palavras apropriadas. O resultado é uma bagunça.
No entanto, se você considerar quais testes os caminhos não cobertos realmente precisam, a ferramenta de cobertura de código terá feito seu trabalho; não prometendo correção, mas apontando alguns dos trabalhos que precisavam ser feitos.
fonte
A cobertura do caminho não pode dizer se todos os recursos necessários foram implementados. Deixar de fora um recurso é um bug, mas a cobertura do caminho não o detectará.
fonte
Parte do problema é que 100% de cobertura garante apenas que o código funcione corretamente após uma única execução . Alguns erros, como vazamentos de memória, podem não ser aparentes ou causar problemas após uma única execução, mas, com o tempo, causarão problemas ao aplicativo.
Por exemplo, digamos que você tenha um aplicativo que se conecte a um banco de dados. Talvez em um método o programador esqueça de fechar a conexão com o banco de dados quando terminar sua consulta. Você pode executar vários testes nesse método e não encontrar erros com a funcionalidade, mas o servidor de banco de dados pode ter um cenário em que está sem conexões disponíveis, porque esse método específico não fechou a conexão quando foi concluído e as conexões abertas devem agora tempo limite.
fonte
times_two(x) = x + 2
, isso será totalmente coberto pelo conjunto de testesassert(times_two(2) == 4)
, mas esse ainda é obviamente um código de buggy! Não há necessidade de vazamentos de memória :)Como já foi dito, a resposta é NÃO.
Além do que está sendo dito, existem bugs aparecendo em diferentes níveis, que não podem ser testados com testes de unidade. Apenas para citar alguns:
fonte
O que significa para cada caminho a ser testado?
As outras respostas são ótimas, mas quero apenas acrescentar que a condição "todo caminho de um programa é testado" é vaga.
Considere este método:
Se você escrever um teste que afirme
add(1, 2) == 3
, uma ferramenta de cobertura de código informará que todas as linhas são exercidas. Mas você realmente não afirmou nada sobre o efeito colateral global ou a atribuição inútil. Essas linhas foram executadas, mas não foram realmente testadas.O teste de mutação ajudaria a encontrar problemas como este. Uma ferramenta de teste de mutação teria uma lista de maneiras pré-determinadas de "alterar" o código e ver se os testes ainda passam. Por exemplo:
+=
para-=
. Essa mutação não causaria uma falha no teste, portanto provaria que seu teste não afirma nada significativo sobre o efeito colateral global.Em essência, os testes de mutação são uma maneira de testar seus testes . Mas, assim como você nunca testará a função real com todos os conjuntos de entradas possíveis, nunca executará todas as mutações possíveis; portanto, novamente, isso é limitado.
Todo teste que podemos fazer é uma heurística para avançar para programas sem erros. Nada é perfeito.
fonte
Bem ... sim , na verdade, se todos os caminhos "através" do programa forem testados. Mas isso significa que todo caminho possível em todo o espaço de todos os estados possíveis que o programa pode ter, incluindo todas as variáveis. Mesmo para um programa estaticamente compilado muito simples - digamos, um antigo triturador de números Fortran - isso não é viável, embora possa pelo menos ser imaginável: se você tiver apenas duas variáveis inteiras, estará basicamente lidando com todas as maneiras possíveis de conectar pontos. uma grade bidimensional; na verdade, parece muito com o Travelling Salesman. Para n tais variáveis, você está lidando com um espaço n- dimensional, portanto, para qualquer programa real, a tarefa é completamente intratável.
Pior: para coisas sérias, você não possui apenas um número fixo de variáveis primitivas, mas cria variáveis dinamicamente em chamadas de função, ou possui variáveis de tamanho variável ... ou qualquer coisa assim, quanto possível, em uma linguagem completa de Turing. Isso torna o espaço de estado infinito-dimensional, destruindo todas as esperanças de cobertura total, mesmo com equipamentos de teste absurdamente poderosos.
Dito isto ... na verdade as coisas não são tão sombrias. Ele é possível proove programas inteiros para ser correto, mas você tem que desistir de algumas idéias.
Primeiro: é altamente recomendável mudar para um idioma declarativo. Linguagens imperativas, por algum motivo, sempre foram de longe as mais populares, mas a maneira como elas misturam algoritmos com interações no mundo real torna extremamente difícil até dizer o que você quer dizer com “correto”.
Muito mais fácil em linguagens de programação puramente funcionais : elas têm uma distinção clara entre as propriedades realmente interessantes das funções matemáticas e as interações nebulosas do mundo real sobre as quais você realmente não pode dizer nada. Para as funções, é muito fácil especificar "comportamento correto": se, para todas as entradas possíveis (dos tipos de argumento), o resultado desejado correspondente sair, a função se comportará corretamente.
Agora, você diz que isso ainda é intratável ... afinal, o espaço de todos os argumentos possíveis também é, em geral, de dimensão infinita. É verdade - embora para uma única função, até os testes de cobertura ingênuos o levem muito mais longe do que você jamais poderia esperar em um programa imperativo! No entanto, existe uma ferramenta poderosa e incrível que muda o jogo: quantificação universal / polimorfismo paramétrico . Basicamente, isso permite que você escreva funções em tipos muito gerais de dados, com a garantia de que, se funcionar para um exemplo simples dos dados, funcionará para qualquer entrada possível.
Pelo menos teoricamente. Não é fácil encontrar os tipos certos que são realmente tão gerais que você pode provar completamente isso - normalmente, você precisa de uma linguagem de tipo dependente , e eles tendem a ser bastante difíceis de usar. Porém, escrever em um estilo funcional apenas com polimorfismo paramétrico já aumenta seu "nível de segurança" de maneira enorme - você não encontrará necessariamente todos os bugs, mas precisará escondê-los muito bem para que o compilador não os encontre!
fonte