Estou escrevendo um código (recursivo) que está navegando em um gráfico de dependência procura por ciclos ou contradições nas dependências. No entanto, não tenho certeza de como abordar isso. O problema é que uma de nossas principais preocupações é o código manipulará todas as estruturas gráficas interessantes que possam surgir e garantir que todos os nós sejam tratados adequadamente.
Embora geralmente 100% de cobertura de linha ou filial seja suficiente para ter certeza de que algum código funciona, parece que mesmo com 100% de cobertura de caminho, você ainda tem dúvidas.
Portanto, como selecionar estruturas de gráfico para casos de teste para ter certeza de que seu código pode lidar com todas as permutações concebíveis que você encontrará nos dados do mundo real.
PS- Se isso importa, todas as arestas do meu gráfico são rotuladas como "devem ter" xor "não podem ter" e não há ciclos triviais, e há apenas uma aresta entre dois nós.
PPS - Esta declaração de problema adicional foi originalmente postada pelo autor da pergunta em um comentário abaixo:
For all vertices N in forest F, for all vertices M, in F, such that if there are any walks between N and M they all must either use only edges labelled 'conflict' or 'requires'.
fonte
Respostas:
Isso soa como um bom começo. Acho que você já tentou aplicar algumas técnicas clássicas, como análise de valor de limite ou particionamento de equivalência , como já mencionou testes baseados em cobertura. Depois de investir muito tempo na construção de bons casos de teste, você chegará a um ponto em que você, sua equipe e também seus testadores (se os tiver) ficarão sem ideias. E é nesse ponto que você deve deixar o caminho dos testes de unidade e começar a testar com o máximo de dados do mundo real possível.
Deveria ser óbvio que você deveria tentar escolher uma grande diversidade de gráficos nos dados de produção. Talvez você precise escrever algumas ferramentas ou programas adicionais apenas para essa parte do processo. A parte difícil aqui é provavelmente verificar a exatidão da saída de seus programas, quando você colocar dez mil gráficos diferentes do mundo real em seu programa, como você saberá se seu programa sempre produz a saída correta? Obviamente, você não pode verificar do que manualmente. Portanto, se você tiver sorte, poderá executar uma segunda implementação muito simples de sua verificação de dependência, o que pode não atender às suas expectativas de desempenho, mas é mais fácil de verificar do que o algoritmo original. Você também deve tentar integrar várias verificações de plausibilidade diretamente no seu programa (por exemplo,
Por fim, aprenda a aceitar que todo teste pode apenas provar a existência de bugs, mas não a ausência de bugs.
fonte
1. Geração de teste randomizado
Escreva um algoritmo que gere gráficos, faça com que gere algumas centenas (ou mais) gráficos aleatórios e jogue cada um no seu algoritmo.
Mantenha a semente aleatória dos gráficos que causam falhas interessantes e adicione-as como testes de unidade.
2. Peças complicadas de código rígido
Algumas estruturas gráficas que você conhece são complicadas, você pode codificar imediatamente ou escrever algum código que as combine e as empurre para o seu algoritmo.
3. Gere lista exaustiva
Mas, se você quiser ter certeza de que "o código pode lidar com todas as permutações concebíveis que você encontrará nos dados do mundo real", será necessário gerar esses dados não a partir de sementes aleatórias, mas caminhando por todas as permutações. (Isso é feito durante o teste de sistemas de sinal de metrô e dá a você enormes quantidades de casos que levam anos para serem testados. Para metrôs, o sistema é limitado, para que haja um limite superior ao número de permutações. Não sei como o seu caso aplica)
fonte
For all vertices N in forest F, for all vertices M, in F, such that if there are any walks between N and M they all must either use only edges labelled 'conflict' or 'requires'.
o domínio não é o problema.Nenhum teste será capaz de ser suficiente nesse caso, nem mesmo toneladas de dados do mundo real ou distorções. Uma cobertura de código de 100% ou até 100% de caminho é insuficiente para testar funções recursivas.
Ou a função recursiva resiste a uma prova formal (nesse caso não deve ser tão difícil) ou não. Se o código estiver muito entrelaçado com o código específico do aplicativo para descartar efeitos colaterais, é por onde começar.
O próprio algoritmo soa como um algoritmo de inundação simples, semelhante a uma primeira pesquisa ampla e simples, com a adição de uma lista negra que não deve se cruzar com a lista de nós visitados, executada em todos os nós.
Esse algoritmo iterativo cumpre a condição de que nenhuma dependência pode ser necessária e colocada na lista negra ao mesmo tempo, para gráficos de estrutura arbitrária, iniciando a partir de qualquer artefato arbitrário pelo qual o artefato inicial é sempre necessário.
Embora possa ser ou não tão rápido quanto sua própria implementação, pode ser comprovado que ele termina em todos os casos (como para cada iteração do loop externo, cada elemento pode ser empurrado apenas uma vez na
tovisit
fila), ele inunda toda a área de alcance gráfico (prova indutiva) e detecta todos os casos em que um artefato precisa ser requerido e colocado na lista negra simultaneamente, iniciando em cada nó.Se você puder mostrar que sua própria implementação tem as mesmas características, poderá provar a correção sem resultar em teste de unidade. Somente os métodos básicos para empurrar e saltar de filas, contando o comprimento da fila, iterando sobre propriedades e similares precisam ser testados e mostrados como livres de efeitos colaterais.
EDIT: O que esse algoritmo não prova é que seu gráfico está livre de ciclos. Os gráficos acíclicos direcionados são um tópico bem pesquisado; portanto, também deve ser fácil encontrar um algoritmo pronto para provar essa propriedade.
Como você pode ver, não há necessidade de reinventar a roda.
fonte
Você está usando frases como "todas as estruturas gráficas interessantes" e "manipuladas adequadamente". A menos que você tenha maneiras de testar seu código em relação a todas essas estruturas e determinar se o código manipula o gráfico adequadamente, você pode usar apenas ferramentas como análise de cobertura de teste.
Sugiro que você comece encontrando e testando várias estruturas de gráficos interessantes, determinando qual seria o manuseio adequado e ver se o código faz isso. Em seguida, você pode começar a perturbar esses gráficos em a) gráficos quebrados que violam regras ou b) gráficos não tão interessantes que apresentam problemas; veja se o seu código falha adequadamente ao manipulá-los.
fonte
Você pode tentar fazer uma classificação topológica e ver se é bem-sucedida. Caso contrário, você terá pelo menos um ciclo.
fonte
Quando se trata desse tipo de algoritmo difícil de testar, eu usaria o TDD, onde você constrói o algoritmo com base em testes,
TDD em resumo,
e repita o ciclo,
Nesta situação em particular,
Um aspecto importante desse método é que você sempre deve adicionar um teste para uma possível etapa (onde você divide os cenários possíveis em testes simples) e, quando você cobre todos os cenários possíveis, o algoritmo geralmente evolui automaticamente.
Finalmente, você precisa adicionar um ou mais testes de integração complicados para verificar se há problemas imprevistos (como erros de estouro de pilha / erros de desempenho quando o gráfico é muito grande e quando você usa recursão)
fonte
Meu entendimento do problema, como originalmente declarado e depois atualizado pelos comentários da resposta de Macke, inclui o seguinte: 1) os dois tipos de borda (dependências e conflitos) são direcionados; 2) se dois nós estiverem conectados por uma borda, eles não deverão estar conectados por outra, mesmo que seja do outro tipo ou ao contrário; 3) se um caminho entre dois nós puder ser construído misturando arestas de tipos diferentes, isso será um erro, e não uma circunstância que será ignorada; 4) Se houver um caminho entre dois nós usando arestas de um tipo, pode não haver outro caminho entre eles usando arestas do outro tipo; 5) ciclos de um tipo de borda única ou de borda mista não são permitidos (de um palpite no aplicativo, não tenho certeza de que os ciclos somente de conflito sejam um erro, mas essa condição pode ser removida, se não).
Além disso, assumirei que a estrutura de dados usada não impede que violações desses requisitos sejam expressas (por exemplo, um gráfico que viole a condição 2 não pôde ser expresso em um mapa do par de nós para (tipo, direção) se o par de nós sempre possui o nó menos numerado primeiro.) Se certos erros não puderem ser expressos, isso reduzirá o número de casos a serem considerados.
Na verdade, existem três gráficos que podem ser considerados aqui: os dois exclusivamente de um tipo de aresta e o gráfico misto formado pela união de um de cada um dos dois tipos. Você pode usar isso para gerar sistematicamente todos os gráficos até um certo número de nós. Primeiro, gere todos os gráficos possíveis de nós N com não mais de uma aresta entre dois pares de nós ordenados (pares ordenados porque esses são gráficos direcionados.) Agora pegue todos os pares possíveis desses gráficos, um representando dependências e outro representando conflitos, e formar a união de cada par.
Se sua estrutura de dados não puder expressar violações da condição 2, você poderá reduzir significativamente os casos a serem considerados, construindo apenas todos os gráficos de conflito possíveis que se encaixem nos espaços dos gráficos de dependência ou vice-versa. Caso contrário, você poderá detectar violações da condição 2 ao formar a união.
Em um percurso amplo do gráfico combinado a partir do primeiro nó, você pode construir o conjunto de todos os caminhos para todos os nós alcançáveis e, ao fazer isso, pode verificar violações de todas as condições (para detecção de ciclo, você pode use o algoritmo de Tarjan .)
Você só precisa considerar os caminhos do primeiro nó, mesmo que o gráfico esteja desconectado, pois em outros casos os caminhos de qualquer outro nó aparecerão como caminhos do primeiro nó.
Se caminhos de borda mista podem simplesmente ser ignorados, em vez de serem erros (condição 3), é suficiente considerar os gráficos de dependência e conflito de forma independente e verificar se, se um nó é alcançável em um, ele não está no outro.
Se você se lembrar dos caminhos encontrados no exame de gráficos de nós N-1, poderá usá-los como ponto de partida para gerar e avaliar gráficos de N nós.
Isso não gera várias arestas do mesmo tipo entre os nós, mas pode ser estendido para isso. No entanto, isso aumentaria muito o número de casos, portanto, seria melhor se o código sendo testado tornasse impossível representar ou, na sua falta, filtrar todos esses casos previamente.
A chave para escrever um oráculo como esse é mantê-lo o mais simples possível, mesmo que isso signifique ser ineficiente, para que você possa estabelecer confiança nele (idealmente através de argumentos para sua correção, apoiados em testes).
Depois de ter os meios para gerar casos de teste e confiar no oráculo que você criou para separar com precisão o bem do mal, você pode usá-lo para conduzir o teste automatizado do código de destino. Se isso não for possível, sua próxima melhor opção é vasculhar os resultados em casos distintos. O oracle pode classificar os erros encontrados e fornecer algumas informações sobre os casos aceitos, como o número e o comprimento dos caminhos de cada tipo, e se há algum nó no início dos dois tipos de caminho, e isso poderia ajudá-lo a procurar casos que você nunca viu antes.
fonte