¿Cómo prueba el código de la unidad usando estructuras gráficas?

18

Estoy escribiendo un código (recursivo) que navega por un gráfico de dependencia para buscar ciclos o contradicciones en las dependencias. Sin embargo, no estoy seguro de cómo abordar la unidad para probar esto. El problema es que una de nuestras principales preocupaciones es que el código maneje todas las estructuras gráficas interesantes que puedan surgir y se asegure de que todos los nodos se manejen de manera adecuada.

Si bien la cobertura del 100% de línea o rama es suficiente para estar seguro de que algunos códigos funcionan, parece que incluso con una cobertura de ruta del 100% aún tendría dudas.

Entonces, ¿cómo se hace para seleccionar las estructuras de gráficos para casos de prueba para tener la confianza de que su código podría manejar todas las permutaciones concebibles que encontrará en los datos del mundo real.


PD: si es importante, todos los bordes en mi gráfico están etiquetados como "debe tener" xo "no puede tener" y no hay ciclos triviales, y solo hay un borde entre dos nodos.


PPS: esta declaración adicional del problema fue publicada originalmente por el autor de la pregunta en un comentario a continuación:

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

Trineo
fuente
13
De la misma forma que prueba la unidad con cualquier otro método. Identifica todos los casos de prueba "interesantes" para cada método y escribe pruebas unitarias para ellos. En su caso, tendrá que crear gráficos de dependencia enlatados para cada una de las estructuras de gráficos "interesantes".
Dunk
@Dunk Seguimos pensando que tenemos todos los problemas cubiertos y luego nos damos cuenta de que cierta estructura causa problemas que no habíamos considerado antes. Prueba de cada complicado que podemos pensar es lo que estamos haciendo, lo que espero es que encontrar algunas directrices / procedimientos para generar molestos ejemplos tal vez usando reducibilidad de las formas fundamentales etc
trineo
66
Ese es el problema con cualquier forma de prueba. Todo lo que sabes es que las pruebas que pensaste en el trabajo. No significa que su sw esté libre de errores solo porque sus pruebas pasen. Cada proyecto tiene ese mismo problema. Estoy en las etapas finales de entrega de mi proyecto actual para que podamos comenzar a fabricar. Los tipos de errores que encontramos ahora tienden a ser bastante oscuros. Por ejemplo, cuando el hardware aún funciona según las especificaciones pero apenas y cuando se combina con otro hardware simultáneamente con el mismo problema, entonces ocurren problemas; pero solo a veces :( El sw está bien probado pero no pensamos en todo
Dunk
Lo que describe parece más una prueba de integración y no una prueba de unidad. Las pruebas unitarias garantizarían que un método pueda encontrar los círculos en un gráfico. Otras pruebas unitarias garantizarían que la clase bajo prueba maneje un círculo específico de un gráfico específico.
SpaceTrucker
La detección de ciclos es un tema bien cubierto (vea Knuth, y también algunas respuestas a continuación) y las soluciones no involucran una gran cantidad de casos especiales, por lo que primero debe determinar qué hace que su problema sea así. ¿Se debe a las contradicciones que mencionas? Si es así, necesitamos más información sobre ellos. Si es el resultado de opciones de implementación, es posible que tenga que refactorizar, tal vez a lo grande. Fundamentalmente, este es un problema de diseño que tendrá que pensar detenidamente, TDD es el enfoque incorrecto que puede llevarlo profundamente al laberinto antes de llegar al callejón sin salida.
sdenham

Respuestas:

5

Seguimos pensando que tenemos todos los aspectos difíciles cubiertos y luego nos damos cuenta de que cierta estructura causa problemas que no habíamos considerado antes. Probar todos los trucos que podemos pensar es lo que estamos haciendo.

Eso suena como un buen comienzo. Supongo que ya trató de aplicar algunas técnicas clásicas como el análisis del valor límite o la partición de equivalencia , como ya mencionó las pruebas basadas en la cobertura. Después de haber invertido mucho tiempo en la construcción de buenos casos de prueba, llegará a un punto en el que usted, su equipo y también sus evaluadores (si los tiene) se queden sin ideas. Y ese es el punto donde debe dejar el camino de las pruebas unitarias y comenzar a probar con la mayor cantidad de datos del mundo real que pueda.

Debería ser obvio que debe intentar elegir una gran diversidad de gráficos de sus datos de producción. Quizás tenga que escribir algunas herramientas o programas adicionales solo para esa parte del proceso. La parte difícil aquí es probablemente verificar la exactitud de la salida de sus programas, cuando coloca diez mil gráficos diferentes del mundo real en su programa, ¿cómo sabrá si su programa produce siempre la salida correcta? Obviamente no puede verificar que manualmente. Entonces, si tiene suerte, puede realizar una segunda implementación muy simple de su verificación de dependencia, que puede no cumplir con sus expectativas de rendimiento, pero es más fácil de verificar que su algoritmo original. También debe intentar integrar muchas verificaciones de plausibilidad directamente en su programa (por ejemplo,

Finalmente, aprenda a aceptar que cada prueba solo puede probar la existencia de errores, pero no la ausencia de errores.

Doc Brown
fuente
5

1. Generación de prueba aleatoria

Escriba un algoritmo que genere gráficos, haga que genere unos cientos (o más) gráficos aleatorios y arroje cada uno a su algoritmo.

Mantenga la semilla aleatoria de los gráficos que causan fallas interesantes y agréguelos como pruebas unitarias.

2. Codificar partes difíciles

Algunas estructuras de gráficos que usted conoce son difíciles de codificar de inmediato, o puede escribir algún código que las combine y las empuje a su algoritmo.

3. Generar lista exhaustiva

Pero, si quiere estar seguro de que "el código podría manejar todas las permutaciones concebibles que encontrará en los datos del mundo real", debe generar estos datos no a partir de una semilla aleatoria, sino caminando a través de todas las permutaciones. (Esto se hace cuando se prueban los sistemas de señalización del tren subterráneo, y le brinda una enorme cantidad de casos que demoran años en probarse. Para los trenes subterráneos, el sistema está acotado, por lo que hay un límite superior para la cantidad de permutaciones. No estoy seguro de cómo es su caso aplica)

Macke
fuente
El interlocutor ha escrito que no pueden decir si han considerado todos los casos, lo que implica que no tienen forma de enumerarlos. Hasta que comprendan el dominio del problema lo suficientemente bien como para hacer eso, cómo evaluar es una cuestión discutible.
sdenham
@sdenham ¿Cómo vas a enumerar algo que literalmente tiene un número infinito de posibles combinaciones válidas? Tenía la esperanza de encontrar algo en la línea de "estas son las estructuras gráficas más difíciles que a menudo detectarán errores en su implementación". Entiendo el dominio lo suficientemente bien como es simple: 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'.el dominio no es el problema.
Trineo
@ArtB: Gracias por aclarar el problema. Como ha dicho, no hay más de un borde entre dos vértices, y aparentemente están descartando caminos con ciclos (o al menos más de una pasada alrededor de cualquier ciclo), entonces al menos sabemos que no hay literalmente un número infinito de posibles combinaciones válidas, que es el progreso. Tenga en cuenta que saber enumerar todas las posibilidades no es lo mismo que decir que debe hacerlo, ya que podría ser un punto de partida para hacer un argumento de corrección, que a su vez puede guiar las pruebas. Lo
pensaré
@ArtB: debe modificar la pregunta para incluir la actualización de la declaración del problema que ha proporcionado aquí. Además, puede ser útil afirmar que estos son bordes dirigidos (si ese es el caso), y si un ciclo se consideraría un error en el gráfico, en lugar de solo una situación que el algoritmo necesita manejar.
sdenham
4

Ninguna prueba en absoluto será suficiente en este caso, ni siquiera toneladas de datos reales o borrosos. Una cobertura de código del 100%, o incluso una cobertura de ruta del 100%, es insuficiente para probar funciones recursivas.

O la función recursiva resiste una prueba formal (no debería ser tan difícil en este caso), o no lo hace. Si el código está demasiado entrelazado con el código específico de la aplicación para descartar efectos secundarios, ahí es donde comenzar.

El algoritmo en sí mismo suena como un algoritmo de inundación simple, similar a una primera búsqueda amplia simple, con la adición de una lista negra que no debe cruzarse con la lista de nodos visitados, que se ejecuta desde todos los nodos.

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

Este algoritmo iterativo cumple la condición de que no se requiera dependencia y se coloque en la lista negra al mismo tiempo, para gráficos de estructura arbitraria, comenzando desde cualquier artefacto arbitrario por el cual el artefacto de inicio siempre se requiera.

Si bien puede o no ser tan rápido como su propia implementación, se puede demostrar que termina en todos los casos (en cuanto a cada iteración del bucle externo, cada elemento solo se puede empujar una vez en la tovisitcola), inunda todo el alcance gráfico (prueba inductiva), y detecta todos los casos en los que se requiere que un artefacto se requiera y se ponga en una lista negra simultáneamente, comenzando desde cada nodo.

Si puede demostrar que su propia implementación tiene las mismas características, puede probar la corrección sin resultar en pruebas unitarias. Solo los métodos básicos para empujar y saltar de las colas, contar la longitud de la cola, iterar sobre propiedades y similares deben probarse y demostrarse que están libres de efectos secundarios.

EDITAR: Lo que este algoritmo no prueba es que su gráfico no tenga ciclos. Sin embargo, los gráficos acíclicos dirigidos son un tema bien investigado, por lo que encontrar un algoritmo listo para probar esta propiedad también debería ser fácil.

Como puede ver, no hay necesidad de reinventar la rueda, en absoluto.

Ext3h
fuente
3

Está utilizando frases como "todas las estructuras gráficas interesantes" y "manejado adecuadamente". A menos que tenga formas de probar su código contra todas esas estructuras y determinar si el código maneja el gráfico adecuadamente, solo puede usar herramientas como el análisis de cobertura de prueba.

Le sugiero que comience buscando y probando con una serie de estructuras gráficas interesantes y determine cuál sería el manejo apropiado y vea que el código lo haga. Luego, puede comenzar a perturbar esas gráficas en a) gráficas rotas que violan las reglas ob) gráficas no tan interesantes que tienen problemas; vea si su código no puede manejarlos correctamente.

BobDalgleish
fuente
Si bien este es un buen enfoque para las pruebas, no resuelve el problema central de la pregunta: cómo garantizar que se cubran todos los casos. Creo que eso requerirá más análisis y posiblemente una refactorización; vea mi pregunta más arriba.
sdenham
2

Cuando se trata de este tipo de algoritmo difícil de probar, iría por el TDD, donde construyes el algoritmo basado en pruebas,

TDD en resumen,

  • escribe la prueba
  • veo que está fallando
  • modificar el código
  • asegúrese de que todas las pruebas estén aprobadas
  • refactor

y repite el ciclo,

En esta situación particular,

  1. La primera prueba sería un gráfico de un solo nodo donde el algoritmo no debería devolver ningún ciclo
  2. El segundo sería un gráfico de tres nodos sin ciclo donde el algoritmo no debería devolver ningún ciclo
  3. El siguiente sería usar un gráfico de tres nodos con un ciclo donde el algoritmo no debería devolver ningún ciclo
  4. Ahora puedes probarlo contra un ciclo un poco más complejo dependiendo de las posibilidades

Un aspecto importante en este método es que siempre debe agregar una prueba para el posible paso (donde divide los escenarios posibles en pruebas simples), y cuando cubre todos los escenarios posibles, el algoritmo generalmente evoluciona automáticamente.

Finalmente, debe agregar una o más pruebas de integración complicadas para ver si hay problemas imprevistos (como errores de desbordamiento de pila / errores de rendimiento cuando su gráfico es muy grande y cuando usa la recursividad)

Pelican de bajo vuelo
fuente
2

Mi comprensión del problema, como se declaró originalmente y luego se actualizó mediante comentarios en la respuesta de Macke, incluye lo siguiente: 1) ambos tipos de borde (dependencias y conflictos) están dirigidos; 2) si dos nodos están conectados por un borde, no deben estar conectados por otro, incluso si es del otro tipo o al revés; 3) si se puede construir una ruta entre dos nodos mezclando bordes de diferentes tipos, entonces eso es un error, en lugar de una circunstancia que se ignora; 4) Si hay una ruta entre dos nodos usando bordes de un tipo, entonces puede que no haya otra ruta entre ellos usando bordes del otro tipo; 5) los ciclos de un tipo de borde único o de tipos de borde mixto no están permitidos (por una suposición en la aplicación, no estoy seguro de que los ciclos de solo conflicto sean un error, pero esta condición se puede eliminar, de lo contrario).

Además, supondré que la estructura de datos utilizada no evita que se expresen violaciones de estos requisitos (por ejemplo, un gráfico que viola la condición 2 no podría expresarse en un mapa de nodo a par (tipo, dirección) si el par de nodos siempre tiene el nodo menos numerado primero.) Si no se pueden expresar ciertos errores, reduce el número de casos a considerar.

En realidad, hay tres gráficos que se pueden considerar aquí: los dos de un solo tipo de borde y el gráfico mixto formado por la unión de uno de cada uno de los dos tipos. Puede usar esto para generar sistemáticamente todos los gráficos hasta cierto número de nodos. Primero, genere todos los gráficos posibles de N nodos que no tengan más de un borde entre dos pares de nodos ordenados (pares ordenados porque son gráficos dirigidos). Ahora tome todos los pares posibles de estos gráficos, uno que representa dependencias y el otro que representa conflictos, y formar la unión de cada par.

Si su estructura de datos no puede expresar violaciones de la condición 2, puede reducir significativamente los casos a considerar construyendo solo todos los gráficos de conflicto posibles que quepan dentro de los espacios de los gráficos de dependencia, o viceversa. De lo contrario, puede detectar violaciones de la condición 2 mientras forma la unión.

En un recorrido transversal del gráfico combinado desde el primer nodo, puede construir el conjunto de todas las rutas a cada nodo accesible, y al hacerlo, puede verificar si hay violaciones de todas las condiciones (para la detección del ciclo, puede usa el algoritmo de Tarjan ).

Solo tiene que considerar las rutas del primer nodo, incluso si el gráfico está desconectado, porque las rutas de cualquier otro nodo aparecerán como rutas del primer nodo en algún otro caso.

Si las rutas de borde mixto pueden simplemente ignorarse, en lugar de ser errores (condición 3), es suficiente considerar las gráficas de dependencia y conflicto de forma independiente, y verificar que si un nodo es accesible en uno, no está en el otro.

Si recuerda las rutas que se encuentran al examinar gráficos de nodos N-1, puede usarlos como punto de partida para generar y evaluar gráficos de N nodos.

Esto no genera múltiples bordes del mismo tipo entre nodos, pero podría extenderse para hacerlo. Sin embargo, esto aumentaría en gran medida el número de casos, por lo que sería mejor si el código que se está probando hace que sea imposible de representar, o si falla, filtra todos los casos de antemano.

La clave para escribir un oráculo como este es mantenerlo lo más simple posible, incluso si eso significa ser ineficiente, de modo que pueda establecer confianza en él (idealmente a través de argumentos para su corrección, respaldado por pruebas).

Una vez que tenga los medios para generar casos de prueba, y confíe en el oráculo que ha creado para separar con precisión lo bueno de lo malo, puede usar esto para conducir las pruebas automatizadas del código objetivo. Si eso no es factible, su siguiente mejor opción es revisar los resultados para casos distintivos. El oráculo puede clasificar los errores que encuentra y proporcionarle información sobre los casos aceptados, como el número y la longitud de las rutas de cada tipo, y si hay nodos al comienzo de ambos tipos de ruta, y esto podría ayudarlo a buscar casos que no haya visto antes.

sdenham
fuente