Como você codifica o teste de unidade usando estruturas de gráficos?

18

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

Trenó
fonte
13
Da mesma forma que você faz o teste unitário de qualquer outro método. Você identifica todos os casos de teste "interessantes" para cada método e escreve testes de unidade para eles. No seu caso, você precisará criar gráficos de dependência em lata para cada uma das estruturas de gráfico "interessantes".
Dunk
@ Dunk Continuamos pensando que temos todos os truques cobertos e depois percebemos que uma certa estrutura causa problemas que não tínhamos considerado antes. Testar todos os truques que podemos pensar é o que estamos fazendo, o que espero encontrar são algumas diretrizes / procedimentos para gerar exemplos problemáticos, talvez usando a redutibilidade de formas fundamentais etc.
Sled
6
Esse é o problema de qualquer forma de teste. Tudo que você sabe é que os testes que você pensou em trabalhar. Isso não significa que seu sw esteja livre de erros apenas porque seus testes são aprovados. Todo projeto tem o mesmo problema. Estou nos estágios finais de entrega do meu projeto atual para que possamos começar a fabricar. Os tipos de erros que encontramos agora tendem a ser bastante obscuros. Por exemplo, onde o hardware ainda trabalha de acordo com as especificações, mas apenas por pouco e quando combinado com outro hardware simultaneamente com o mesmo problema, ocorrem problemas; mas apenas algumas vezes :( O sw é bem testado, mas não pensamos em tudo #
Dunk
O que você descreve soa mais como teste de integração e não como teste de unidade. Os testes de unidade garantiriam que um método pudesse encontrar os círculos em um gráfico. Outros testes de unidade garantiriam que um círculo específico de um gráfico específico fosse tratado pela classe em teste.
SpaceTrucker
A detecção de ciclo é um tópico bem coberto (consulte Knuth, e também algumas respostas abaixo) e as soluções não envolvem um grande número de casos especiais; portanto, você deve primeiro determinar o que torna seu problema assim. É devido às contradições que você mencionou? Nesse caso, precisamos de mais informações sobre eles. Se for o resultado de opções de implementação, talvez seja necessário refatorar, talvez em grande escala. Fundamentalmente, este é um problema de design no qual você terá que pensar, o TDD é a abordagem errada que pode levá-lo ao fundo do labirinto antes de chegar ao beco sem saída.
Sdamham 13/04

Respostas:

5

Continuamos pensando que temos todas as questões complicadas cobertas e depois percebemos que uma certa estrutura causa problemas que não tínhamos considerado antes. Testar todos os truques que podemos pensar é o que estamos fazendo.

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.

Doc Brown
fonte
5

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)

Macke
fonte
O questionador escreveu que eles são incapazes de dizer se consideraram todos os casos, o que implica que não há uma maneira de enumerá-los. Até que eles entendam o domínio do problema suficientemente bem para fazer isso, como testar é uma questão discutível.
sdenham
@sdenham Como você vai enumerar algo que literalmente tem um número infinito de combinações válidas possíveis? Eu esperava encontrar algo do tipo "estas são as estruturas gráficas mais complicadas que geralmente detectam bugs em sua implementação". Entendo bem o domínio, pois é simples: 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.
Sled
@ArtB: Obrigado pelo seu esclarecimento do problema. Como você disse que não há mais de uma aresta entre dois vértices e aparentemente está descartando caminhos com ciclos (ou pelo menos mais de uma passagem por qualquer ciclo), então pelo menos sabemos que não há literalmente um número infinito de possíveis combinações válidas, que são progresso. Observe que saber enumerar todas as possibilidades não é o mesmo que dizer que você deve fazê-lo, pois pode ser um ponto de partida para argumentar a correção, o que por sua vez pode orientar o teste. Vou dar-lhe mais pensamento ...
sdenham
@ArtB: Você deve modificar a pergunta para incluir a atualização na declaração do problema que você forneceu aqui. Além disso, pode ser útil afirmar que essas são arestas direcionadas (se for esse o caso) e se um ciclo seria considerado um erro no gráfico, em vez de apenas uma situação que o algoritmo precisa tratar.
sdenham
4

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.

foreach nodes as node
    foreach nodes as tmp
        tmp.status = unmarked

    tovisit = []
    tovisit.push(node)
    node.status = required

    while |tovisit| > 0 do
        next = tovisit.pop()
        foreach next.requires as requirement
            if requirement.status = unmarked
                tovisit.push(requirement)
                requirement.status = required
            else if requirement.status = blacklisted
                return false
        foreach next.collides as collision
            if collision.status = unmarked
                requirement.status = blacklisted
            else if requirement.status = required
                return false
return true

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 tovisitfila), 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.

Ext3h
fonte
3

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.

BobDalgleish
fonte
Embora essa seja uma boa abordagem para o teste, ela não resolve o problema central da pergunta: como garantir que todos os casos sejam cobertos. Acredito que isso exigirá mais análise e possivelmente refatoração - veja minha pergunta acima.
sdenham
2

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,

  • escreva o teste
  • veja que está falhando
  • modificar o código
  • verifique se todos os testes estão passando
  • refatorar

e repita o ciclo,

Nesta situação em particular,

  1. O primeiro teste seria: gráfico de nó único em que o algoritmo não deveria retornar nenhum ciclo
  2. O segundo seria um gráfico de três nós sem ciclo, em que o algoritmo não deveria retornar nenhum ciclo
  3. O próximo seria usar o gráfico de três nós com um ciclo em que o algoritmo não deveria retornar nenhum ciclo
  4. Agora você pode testá-lo em um ciclo um pouco mais complexo, dependendo das possibilidades

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)

Pelicano-voador baixo
fonte
2

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.

Sdenham
fonte