¿La cobertura del camino garantiza encontrar todos los errores?

63

Si se comprueban todas las rutas a través de un programa, ¿eso garantiza encontrar todos los errores?

Si no, ¿por qué no? ¿Cómo podría pasar por cada combinación posible de flujo de programa y no encontrar el problema si existe uno?

Dudo en sugerir que se puedan encontrar "todos los errores", pero tal vez sea porque la cobertura de la ruta no es práctica (ya que es combinatoria), ¿por lo que no se experimenta nunca?

Nota: este artículo ofrece un breve resumen de los tipos de cobertura Mientras pienso en ellos.

    
pregunta 30.03.2015 - 00:56

11 respuestas

128
  

Si se comprueban todas las rutas a través de un programa, ¿eso garantiza encontrar todos los errores?

No

  

Si no, ¿por qué no? ¿Cómo podría pasar por cada combinación posible de flujo de programa y no encontrar el problema si existe uno?

Porque incluso si prueba todas las rutas posibles, aún no las ha probado con todos los valores o todas las combinaciones de valores posibles . Por ejemplo (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
  

Hace ya dos décadas que se señaló que las pruebas del programa pueden demostrar convincentemente la presencia de errores, pero nunca pueden demostrar su ausencia. Después de citar este comentario tan publicitado, el ingeniero de software regresa a la orden del día y continúa refinando sus estrategias de prueba, al igual que el alquimista de antaño, quien continuó refinando sus purificaciones crisocósmicas.

     

- E. W. Dijkstra (énfasis añadido. Escrito en 1988. Han pasado considerablemente más de 2 décadas.)

    
respondido por el Mason Wheeler 30.03.2015 - 01:11
69

Además de la respuesta de Mason , también hay otro problema: la cobertura no le dice qué código se probó, le dice qué código fue ejecutado .

Imagina que tienes un examen con 100% de cobertura de ruta. Ahora elimine todas las aserciones y ejecute el testuite nuevamente. Voilà, el testinguite todavía tiene una cobertura de ruta del 100%, pero no prueba absolutamente nada.

    
respondido por el Jörg W Mittag 30.03.2015 - 02:04
34

Aquí hay un ejemplo más simple para redondear las cosas. Considere el siguiente algoritmo de clasificación (en Java):

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

Ahora, probemos:

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

Ahora, considere que (A) esta llamada particular a sort devuelve el resultado correcto, (B) esta prueba ha cubierto todas las rutas de código.

Pero, obviamente, el programa no se ordena realmente.

De ello se deduce que la cobertura de todas las rutas de código no es suficiente para garantizar que el programa no tenga errores.

    
respondido por el Atsby 30.03.2015 - 09:55
12

Considere la función abs , que devuelve el valor absoluto de un número. Aquí hay una prueba (Python, imagine un marco de prueba):

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

Esta implementación es correcta, pero solo obtiene un 60% de cobertura de código:

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

Esta implementación es incorrecta, pero obtiene una cobertura de código del 100%:

def abs(x):
    return -x
    
respondido por el RemcoGerlich 30.03.2015 - 10:18
7

Otra adición a La respuesta de Mason , el comportamiento de un programa puede depender del entorno de ejecución.

El siguiente código contiene un Uso Después de Libre:

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

Este código es un comportamiento indefinido, dependiendo de la configuración (versión | depuración), sistema operativo y compilador producirá diferentes comportamientos. No solo la cobertura de ruta no garantiza que encontrará la UAF, sino que su conjunto de pruebas no cubrirá los diversos comportamientos posibles de la UAF que dependen de la configuración.

En otra nota, incluso si la cobertura de ruta garantizara encontrar todos los errores, es poco probable que se pueda lograr en la práctica en cualquier programa. Considera el siguiente:

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

Si tu suite de prueba puede generar todas las rutas para esto, entonces felicitaciones eres un criptógrafo.

    
respondido por el dureuill 30.03.2015 - 17:17
7

De las otras respuestas, queda claro que la cobertura de código al 100% en las pruebas no significa que el código sea correcto al 100%, o incluso que se encontrarán todos los errores que se pueden encontrar mediante la prueba (no importa los errores que no puedan detectarse).

Otra forma de responder a esta pregunta es mediante la práctica:

Hay, en el mundo real, y de hecho en su propia computadora, muchas piezas de software que se desarrollan utilizando un conjunto de pruebas que brindan una cobertura del 100% y que aún tienen errores, incluidos los que mejor prueban se identificaría.

Por lo tanto, una pregunta relacionada es:

  

¿Cuál es el punto de las herramientas de cobertura de código?

Las herramientas de cobertura de código ayudan a identificar las áreas que uno ha descuidado probar. Eso puede estar bien (el código es demostrablemente correcto incluso sin pruebas), puede ser imposible de resolver (por alguna razón no se puede encontrar una ruta), o puede ser la ubicación de un gran error apestoso ahora o siguiendo modificaciones futuras. / p>

De alguna manera, el corrector ortográfico es comparable: algo puede "pasar" el corrector ortográfico y puede estar mal escrito de forma que coincida con una palabra en el diccionario. O puede "fallar" porque las palabras correctas no están en el diccionario. O puede pasar y ser un completo disparate. El corrector ortográfico es una herramienta que lo ayuda a identificar los lugares que puede haber perdido en su corrección de pruebas, pero al igual que no puede garantizar una correcta y completa corrección de pruebas, la cobertura de código no puede garantizar pruebas completas y correctas.

Y, por supuesto, la forma incorrecta de usar el corrector ortográfico es la de ir con cada sugerencia, por ejemplo, lo que sugiere que el problema se agrave, entonces si lo dejamos en préstamo.

Con la cobertura de código puede ser tentador, especialmente si tienes un 98% casi perfecto, rellenar los casos para que los caminos restantes se encuentren.

Eso es el equivalente de corregir con la corrección ortográfica que todas las palabras están relacionadas con el tiempo o con un nudo, son todas las palabras apropiadas. El resultado es un lío de ducking.

Sin embargo, si considera qué pruebas realmente necesitan las rutas no cubiertas, la herramienta de cobertura de código habrá hecho su trabajo; no en prometerle corrección, pero indica parte del trabajo que se necesita hacer.

    
respondido por el Jon Hanna 31.03.2015 - 11:26
4

La cobertura de ruta no puede decirle si se han implementado todas las funciones requeridas. Dejar una característica es un error, pero la cobertura de la ruta no la detectará.

    
respondido por el Pete Becker 30.03.2015 - 21:10
4

Parte del problema es que la cobertura al 100% solo garantiza que el código funcionará correctamente después de una ejecución única . Algunos errores, como las fugas de memoria, pueden no ser evidentes o causar problemas después de una sola ejecución, pero con el tiempo causarán problemas para la aplicación.

Por ejemplo, supongamos que tiene una aplicación que se conecta a una base de datos. Tal vez en un método el programador olvide cerrar la conexión a la base de datos cuando hayan terminado con su consulta. Puede ejecutar varias pruebas sobre este método y no encontrar errores en su funcionalidad, pero su servidor de base de datos puede encontrarse en un escenario donde no hay conexiones disponibles porque este método en particular no cerró la conexión cuando se realizó y las conexiones abiertas deben ahora tiempo de espera.

    
respondido por el Derek W 30.03.2015 - 20:20
3
  

Si se comprueban todas las rutas a través de un programa, ¿eso garantiza encontrar todos los errores?

Como ya se dijo, la respuesta es NO.

  

Si no, ¿por qué no?

Además de lo que se dice, hay errores que aparecen en diferentes niveles, que no se pueden probar con pruebas unitarias. Solo por mencionar algunos:

  • errores detectados con pruebas de integración (las pruebas unitarias no deberían usar recursos reales después de todo)
  • errores en los requisitos
  • errores en diseño y arquitectura
respondido por el BЈовић 31.03.2015 - 08:58
2

¿Qué significa para cada ruta que se va a probar?

Las otras respuestas son geniales, pero solo quiero agregar que la condición "cada ruta a través de un programa se prueba" es en sí vaga.

Considera este método:

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

Si escribe una prueba que afirma add(1, 2) == 3 , una herramienta de cobertura de código le indicará que se ejerce cada línea. Pero en realidad no ha afirmado nada sobre el efecto secundario global o la asignación inútil. Esas líneas se ejecutaron, pero en realidad no se han probado.

Las pruebas de mutación ayudarían a encontrar este tipo de problemas. Una herramienta de prueba de mutación tendría una lista de formas predeterminadas para "mutar" el código y ver si las pruebas aún pasan. Por ejemplo:

  • Una mutación podría cambiar el += a -= . Esa mutación no causaría una falla en la prueba, por lo que probaría que su prueba no afirma nada significativo sobre el efecto secundario global.
  • Otra mutación podría eliminar la primera línea. Esa mutación no causaría una falla en la prueba, por lo que probaría que su prueba no afirma nada significativo sobre la tarea.
  • Otra mutación podría eliminar la tercera línea. Eso causaría una falla en la prueba, que en este caso muestra que su prueba afirma algo sobre esa línea.

En esencia, las pruebas de mutación son una forma de probar tus pruebas . Pero al igual que nunca probará la función real con cada conjunto posible de entradas, nunca ejecutará todas las mutaciones posibles, así que de nuevo, esto es limitado.

Cada prueba que podemos hacer es una heurística para avanzar hacia programas libres de errores. Nada es perfecto.

    
respondido por el Nathan Long 01.04.2015 - 17:45
0

Bueno ... en realidad, si cada ruta "a través" del programa se prueba. Pero eso significa que todos los caminos posibles a través del espacio completo de todos los estados posibles que puede tener el programa, incluidas todas las variables. Incluso para un programa muy simple compilado estáticamente, por ejemplo, un antiguo analizador de números de Fortran, eso no es factible, aunque puede ser al menos imaginable: si tiene solo dos variables enteras, básicamente está tratando con todas las formas posibles de conectar puntos en una cuadrícula bidimensional; En realidad, se parece mucho a un vendedor ambulante. Para n tales variables, estás tratando con un espacio dimensional n , por lo que para cualquier programa real, la tarea es completamente imposible de encontrar.

Peor: para cosas serias, usted tiene no solo un número fijo de variables primitivas, pero crea variables sobre la marcha en llamadas a funciones, o tiene variables de tamaño variable ... o algo por el estilo , como sea posible en un lenguaje completo de Turing. Eso hace que el espacio estatal sea infinito-dimensional, destruyendo todas las esperanzas de una cobertura total, incluso con un equipo de prueba absurdamente poderoso.

Dicho esto ... en realidad las cosas no son tan sombrías. Es posible probar programas completos para que sean correctos, pero tendrás que renunciar a algunas ideas.

Primero: es muy recomendable cambiar a un lenguaje declarativo. Los lenguajes imperativos, por alguna razón, siempre han sido, con mucho, los más populares, pero la forma en que combinan los algoritmos con las interacciones del mundo real hace que sea extremadamente difícil decir lo que quiere decir con "correcto".

Mucho más fácil en lenguajes de programación puramente funcionales : estos tienen una clara distinción entre las propiedades interesantes reales de matemática funciones , y las interacciones difusas del mundo real de las que realmente no se puede decir nada. Para las funciones, es muy fácil especificar el "comportamiento correcto": si para todas las entradas posibles (de los tipos de argumentos) sale el resultado deseado correspondiente, la función se comporta correctamente.

Ahora, dices que todavía es intratable ... después de todo, el espacio de todos los argumentos posibles es en general también infinito-dimensional. Cierto, aunque para una sola función, incluso las pruebas de cobertura ingenuas lo llevan mucho más lejos de lo que podría esperar en un programa imperativo. Sin embargo, hay una herramienta increíblemente poderosa que cambia el juego: cuantificación universal / polimorfismo paramétrico . Básicamente, esto le permite escribir funciones en tipos de datos muy generales, con la garantía de que si funciona para un ejemplo simple de los datos, funcionará para cualquier entrada posible.

Al menos en teoría. No es fácil encontrar los tipos correctos que son realmente tan generales que puede probarlo por completo. Por lo general, necesita un lenguaje de tipo dependiente , y estos tienden a ser bastante difíciles de usar. Pero escribir en un estilo funcional solo con polimorfismo paramétrico aumenta tu "nivel de seguridad" enormemente. ¡No necesariamente encontrarás todos los errores, pero tendrás que esconderlos bastante bien para que el compilador no los detecte!

    
respondido por el leftaroundabout 02.04.2015 - 15:58

Lea otras preguntas en las etiquetas