A cobertura do caminho garante encontrar todos os erros?

64

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.


fonte
33
Isso é equivalente ao problema de parada .
31
E se o código que deveria estar lá, não estiver?
RemcoGerlich 30/03
6
@ Snowman: Não, não é. Não é possível resolver o problema de interrupção para todos os programas, mas para muitos programas específicos, é solucionável. Para esses programas, todos os caminhos de código podem ser enumerados em uma quantidade finita (embora possivelmente longa) de tempo.
Jørgen Fogh
3
@ JørgenFogh Mas, ao tentar encontrar erros em qualquer programa, não é a priori desconhecido se o programa é interrompido ou não? Não é essa pergunta sobre o método geral de "encontrar todos os erros em qualquer programa via cobertura de caminho"? Nesse caso, não é semelhante a "descobrir se algum programa é interrompido"?
Andres F.
11
@AndresF. não se sabe se o programa é interrompido se o subconjunto do idioma em que está escrito for capaz de expressar um programa sem interrupção. Se o seu programa for escrito em C sem usar loops / recursão / setjmp etc. ilimitados, ou em Coq ou em ESSL, ele deverá parar e todos os caminhos poderão ser rastreados. (Turing-completude está seriamente superestimada)
Leushenko

Respostas:

128

Se todo caminho de um programa for testado, isso garante a localização de todos os bugs?

Não

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?

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):

def Add(x as Int32, y as Int32) as Int32:
   return x + y

Test.Assert(Add(2, 2) == 4) //100% test coverage
Add(MAXINT, 5) //Throws an exception, despite 100% test coverage

Agora faz duas décadas desde que foi apontado que o teste do programa pode demonstrar de forma convincente a presença de bugs, mas nunca pode demonstrar sua ausência. Depois de citar essa observação bem divulgada com devoção, o engenheiro de software volta à ordem do dia e continua a refinar suas estratégias de teste, assim como o alquimista de outrora, que continuou a refinar suas purificações crisocósmicas.

- EW Dijkstra (Ênfase adicionada. Escrito em 1988. Já se passaram consideravelmente mais de duas décadas.)

Mason Wheeler
fonte
7
@ digitgopher: suponho, mas se um programa não tem entrada, que coisa útil ele faz?
Mason Wheeler
34
Há também a possibilidade de falta de testes de integração, erros nos testes, erros nas dependências, erros no sistema de criação / implantação ou erros na especificação / requisitos originais. Você nunca pode garantir encontrar todos os erros.
Ixrec 29/03/2015
11
@Ixrec: SQLite faz um esforço bastante valioso! Mas veja como é um esforço enorme! Isso não seria adequado para grandes bases de código.
Mason Wheeler
13
Não apenas você não testou todos os valores possíveis ou combinações dos mesmos, como também não testou todos os tempos relativos, alguns dos quais podem expor as condições da corrida ou até fazer seu teste entrar em um impasse, o que faria com que não reportasse nada . Nem seria um fracasso!
Iwillnotexist Idonotexist
14
Minha lembrança (reforçada por escritos como esse ) é que Dijkstra acreditava que, em boas práticas de programação, a prova de que um programa está correto (sob todas as condições) deve ser parte integrante do desenvolvimento do programa. Visto desse ponto de vista, testar é como alquimia. Em vez de hipérbole, acho que essa foi uma opinião muito forte expressa em uma linguagem muito forte.
David K
71

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.

Jörg W Mittag
fonte
2
Isso pode garantir que não haja exceção ao chamar o código testado (com os parâmetros no teste). Isso é um pouco mais do que nada.
Paŭlo Ebermann
7
@ PaŭloEbermann Concordou, um pouco mais do que nada. No entanto, é tremendamente inferior a "encontrar todos os erros";)
Andres F.
11
@ PaŭloEbermann: Exceções são um caminho de código. Se o código puder ser lançado, mas com determinados dados de teste não for lançado, o teste não alcançará 100% de cobertura do caminho. Isso não é específico para exceções como um mecanismo de tratamento de erros. Visual Basic do ON ERROR GOTOtambém é um caminho, como é C do if(errno).
MSalters
11
@ MSalters Estou falando de código que (por especificação) não deve gerar nenhuma exceção, independentemente da entrada. Se lançar algum, isso seria um bug. Obviamente, se você tiver um código especificado para lançar uma exceção, isso deverá ser testado. (E, é claro, como Jörg disse, apenas verificar se o código não gera uma exceção geralmente não é suficiente para garantir que ele faça a coisa certa, mesmo para códigos que não são lançados.) E algumas exceções podem ser lançadas por um não caminho do código visível, como para desreferenciamento de ponteiro nulo ou divisão por zero. A sua ferramenta de cobertura de caminho as captura?
Paŭlo Ebermann
2
Esta resposta diz tudo. Eu levaria a alegação ainda mais longe e diria que, devido a isso, a cobertura do caminho nunca garante encontrar nem um único bug. Existem métricas que podem garantir pelo menos que as alterações sejam detectadas, no entanto - o teste de mutação pode realmente garantir que (algumas) modificações do código serão detectadas.
eis
34

Aqui está um exemplo mais simples para encerrar as coisas. Considere o seguinte algoritmo de classificação (em Java):

int[] sort(int[] x) { return new int[] { x[0] }; }

Agora, vamos testar:

sort(new int[] { 0xCAFEBABE });

Agora, considere que (A) essa chamada específica sortretorna 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.

Atsby
fonte
12

Considere a absfunção, que retorna o valor absoluto de um número. Aqui está um teste (Python, imagine alguma estrutura de teste):

def test_abs_of_neg_number_returns_positive():
    assert abs(-3) == 3

Esta implementação está correta, mas obtém apenas 60% de cobertura de código:

def abs(x):
    if x < 0:
        return -x
    else:
        return x

Esta implementação está incorreta, mas obtém 100% de cobertura do código:

def abs(x):
    return -x
RemcoGerlich
fonte
2
Aqui está outra implementação que passa no teste (perdoe o Python sem quebra de linha): def abs(x): if x == -3: return 3 else: return 0Você pode se livrar da else: return 0parte e obter 100% de cobertura, mas a função seria essencialmente inútil, mesmo que passe no teste de unidade.
a CVn
7

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:

int main(void)
{
    int* a = malloc(sizeof(a));
    int* b = a;
    *a = 0;
    free(a);
    *b = 12; /* UAF */
    return 0;
}

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:

int main(int a, int b)
{
    if (a != b) {
        if (cryptohash(a) == cryptohash(b)) {
            return ERROR;
        }
    }
    return 0;
} 

Se sua suíte de testes pode gerar todos os caminhos para isso, parabéns por ser um criptógrafo.

dureuill
fonte
Fácil para suficientemente pequenos números inteiros :)
CodesInChaos
Sem saber nada cryptohash, é um pouco difícil dizer o que é "suficientemente pequeno". Talvez demore dois dias para concluir em um supercalculador. Mas sim, intpode ser um pouco short.
dureuill
Com números inteiros de 32 bits e hashes criptográficos típicos (SHA2, SHA3 etc.), a computação deve ser bastante barata. Alguns segundos mais ou menos.
CodesInChaos
7

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, é:

Qual é o objetivo das ferramentas de cobertura de código?

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.

Jon Hanna
fonte
+1 Gostei desta resposta porque é construtiva e menciona alguns dos benefícios da cobertura.
Andres F.
4

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á.

Pete Becker
fonte
11
Eu acho que isso depende da definição de um bug. Eu não acho que os recursos ou funcionalidades ausentes devam ser considerados bugs.
eis
@eis - você não vê um problema com um produto cuja documentação diz que ele faz X quando na verdade não? Essa é uma definição bastante restrita de "bug". Quando gerenciei o controle de qualidade da linha de produtos C ++ da Borland, não éramos tão generosos.
Pete Becker
Eu não vejo por que a documentação diz que faz X, se isso nunca foi implementado
eis
@eis - se o design original pedisse o recurso X, a documentação poderia acabar descrevendo o recurso X. Se ninguém o implementou, isso é um bug e a cobertura do caminho (ou qualquer outro tipo de teste de caixa preta) não o encontrará.
Pete Becker
Ops, a cobertura do caminho é teste em caixa branca , não em caixa preta . O teste da caixa branca não pode capturar os recursos ausentes.
Pete Becker
4

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.

Derek W
fonte
Concordamos que isso faz parte da questão, mas a verdadeira questão é mais fundamental que isso. Mesmo com um computador teórico com memória infinita e sem simultaneidade, 100% da cobertura do teste não implica na ausência de bugs. Exemplos triviais disso abundam nas respostas aqui, mas aqui está outro: se meu programa for times_two(x) = x + 2, isso será totalmente coberto pelo conjunto de testes assert(times_two(2) == 4), mas esse ainda é obviamente um código de buggy! Não há necessidade de vazamentos de memória :)
Andres F.
2
É um ponto importante e reconheço que é um prego maior / mais fundamental no caixão da possibilidade de aplicativos sem bugs, mas como você diz, ele já foi adicionado aqui e eu queria adicionar algo que não estava completamente coberto respostas existentes. Ouvi falar de aplicativos que falharam porque as conexões com o banco de dados não foram liberadas novamente no pool de conexões quando não eram mais necessárias - Um vazamento de memória é apenas o exemplo canônico de má administração de recursos. Meu argumento foi acrescentar que o gerenciamento adequado de recursos em geral não pode ser totalmente testado.
Derek W
Bom ponto. Acordado.
Andres F.
3

Se todo caminho de um programa for testado, isso garante a localização de todos os bugs?

Como já foi dito, a resposta é NÃO.

Se não, por que 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:

  • erros detectados nos testes de integração (afinal, os testes de unidade não devem usar recursos reais)
  • bugs nos requisitos
  • bugs em design e arquitetura
BЈовић
fonte
2

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:

def add(num1, num2)
  foo = "bar"  # useless statement
  $global += 1 # side effect
  num1 + num2  # actual work
end

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:

  • Uma mutação pode mudar +=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.
  • Outra mutação pode excluir a primeira linha. Essa mutação não causaria uma falha no teste, portanto provaria que seu teste não afirma nada significativo sobre a tarefa.
  • Ainda outra mutação pode excluir a terceira linha. Isso causaria uma falha no teste, o que, neste caso, mostra que seu teste afirma algo sobre essa linha.

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.

Nathan Long
fonte
0

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!

à esquerda
fonte
Discordo da sua primeira frase. Passar por todos os estados do programa, por si só, não detecta nenhum erro. Mesmo que você verifique falhas e erros explícitos, ainda assim não verificou a funcionalidade real de forma alguma, apenas cobriu uma pequena parte do espaço de erro.
Matthew Leia
@ MatthewRead: se você aplicar isso consequentemente, o "espaço de erro" será um subespaço adequado do espaço de todos os estados. É claro que é hipotético, porque mesmo os estados "corretos" ocupam um espaço muito grande para permitir testes exaustivos.
precisa saber é o seguinte