Si se prueba cada ruta a través de un programa, ¿eso garantiza encontrar todos los errores?
¿Si no, porque no? ¿Cómo podría pasar por todas las combinaciones posibles de flujo de programa y no encontrar el problema si existe?
Dudo en sugerir que se pueden encontrar "todos los errores", pero tal vez eso se deba a que la cobertura de ruta no es práctica (ya que es combinatoria), por lo que nunca se experimenta.
Nota: este artículo ofrece un resumen rápido de los tipos de cobertura cuando pienso en ellos.
Respuestas:
No
Porque incluso si prueba todas las rutas posibles , aún no las ha probado con todos los valores posibles o todas las combinaciones posibles de valores . Por ejemplo (pseudocódigo):
fuente
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 se ejecutó .
Imagine que tiene un traje de prueba con una cobertura de ruta del 100%. Ahora elimine todas las afirmaciones y vuelva a ejecutar el testuite. Voilà, el testuite todavía tiene una cobertura de ruta del 100%, pero no prueba absolutamente nada.
fuente
ON ERROR GOTO
también es un camino, como lo es C'sif(errno)
.Aquí hay un ejemplo más simple para redondear las cosas. Considere el siguiente algoritmo de clasificación (en Java):
Ahora, probemos:
Ahora, considere que (A) esta llamada particular a
sort
devuelve el resultado correcto, (B) todas las rutas de código han sido cubiertas por esta prueba.Pero, obviamente, el programa en realidad no se ordena.
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.
fuente
Considere la
abs
función, que devuelve el valor absoluto de un número. Aquí hay una prueba (Python, imagine un marco de prueba):Esta implementación es correcta, pero solo obtiene un 60% de cobertura de código:
Esta implementación es incorrecta, pero obtiene una cobertura de código del 100%:
fuente
def abs(x): if x == -3: return 3 else: return 0
posiblemente podría eludir laelse: return 0
parte y obtener una cobertura del 100%, pero la función sería esencialmente inútil a pesar de que pasa la prueba de la unidad.Otra adición a la respuesta de Mason , el comportamiento de un programa puede depender del entorno de tiempo de ejecución.
El siguiente código contiene Use-After-Free:
Este código es Comportamiento indefinido, según la configuración (versión | depuración), el sistema operativo y el compilador producirá diferentes comportamientos. No solo la cobertura de ruta no garantizará que encontrará el UAF, sino que su conjunto de pruebas generalmente no cubrirá los diversos comportamientos posibles del 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. Considere el siguiente:
Si su conjunto de pruebas puede generar todas las rutas para esto, felicidades, usted es un criptógrafo.
fuente
cryptohash
, es un poco difícil decir qué es "suficientemente pequeño". Tal vez demore dos días en completarse en un supercalculador. Pero sí,int
podría resultar un pocoshort
.Está claro por las otras respuestas que la cobertura del 100% del código en las pruebas no significa una corrección del 100% del código, o incluso que se encontrarán todos los errores que se podrían encontrar mediante la prueba (no importa los errores que ninguna prueba podría detectar).
Otra forma de responder a esta pregunta es la práctica:
En el mundo real, y de hecho en su propia computadora, hay 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 errores que las pruebas mejores identificarían.
Una pregunta implícita, por lo tanto, es:
Las herramientas de cobertura de código ayudan a identificar áreas que uno no ha probado. 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 después de futuras modificaciones.
De alguna manera, el corrector ortográfico es comparable: algo puede "pasar" el corrector ortográfico y estar mal escrito de manera 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 completamente absurdo. El corrector ortográfico es una herramienta que lo ayuda a identificar lugares que puede haber pasado por alto en su lectura de prueba, pero al igual que no puede garantizar una lectura de prueba completa y correcta, la cobertura de código no puede garantizar una prueba completa y correcta.
Y, por supuesto, la forma incorrecta de usar el corrector ortográfico es ir con cada sugerencia que evoquemos, por lo que la situación de empeoramiento se vuelve peor que si le dejáramos un préstamo.
Con la cobertura del código, puede ser tentador, especialmente si tiene un 98% casi perfecto, completar los casos para que las rutas restantes se vean afectadas.
Eso es el equivalente de enderezar con un corrector ortográfico que todas las palabras son clima o nudo, son todas las palabras apropiadas. El resultado es un desastre de esquivar.
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, sino en señalar algunos de los trabajos que deben hacerse.
fuente
La cobertura de ruta no puede decirle si se han implementado todas las características requeridas. Dejar de lado una función es un error, pero la cobertura de ruta no lo detectará.
fuente
Parte del problema es que el 100% de cobertura solo garantiza que el código funcionará correctamente después de una sola ejecución . Algunos errores, como las pérdidas de memoria, pueden no ser aparentes o causar problemas después de una sola ejecución, pero con el tiempo causarán problemas a la aplicación.
Por ejemplo, supongamos que tiene una aplicación que se conecta a una base de datos. Quizás en un método, el programador se olvida de cerrar la conexión a la base de datos cuando termina su consulta. Puede ejecutar varias pruebas sobre este método y no encontrar errores con su funcionalidad, pero su servidor de base de datos puede encontrarse con un escenario en el que no hay conexiones disponibles porque este método en particular no cerró la conexión cuando se hizo y las conexiones abiertas deben ahora tiempo de espera.
fuente
times_two(x) = x + 2
, esto estará completamente cubierto por el conjunto de pruebasassert(times_two(2) == 4)
, ¡pero esto obviamente es un código defectuoso! No hay necesidad de pérdidas de memoria :)Como ya se dijo, la respuesta es 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:
fuente
¿Qué significa que cada camino sea probado?
Las otras respuestas son geniales, pero solo quiero agregar que la condición "se prueba cada ruta a través de un programa" es vaga.
Considere este método:
Si escribe una prueba que afirma
add(1, 2) == 3
, una herramienta de cobertura de código le dirá que cada línea se ejerce. Pero en realidad no ha afirmado nada sobre el efecto secundario global o la asignación inútil. Esas líneas ejecutadas, pero realmente no han sido probadas.Las pruebas de mutación ayudarían a encontrar problemas como este. 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:
+=
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.En esencia, las pruebas de mutación son una forma de evaluar sus pruebas . Pero al igual que nunca probará la función real con cada posible conjunto de entradas, nunca ejecutará todas las mutaciones posibles, por lo que, nuevamente, esto es limitado.
Cada prueba que podemos hacer es una heurística para avanzar hacia programas libres de errores. Nada es perfecto.
fuente
Bueno ... sí , en realidad, si se prueba cada ruta "a través" del programa. Pero eso significa que cada ruta posible a través del espacio completo de todos los estados posibles que puede tener el programa, incluidas todas las variables. Incluso para un programa compilado estáticamente muy simple, por ejemplo, un viejo generador de números Fortran, eso no es factible, aunque al menos puede ser imaginable: si solo tiene 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 de tales variables, se trata de un espacio n -dimensional, por lo que para cualquier programa real, la tarea es completamente imposible de manejar.
Peor aún: para cosas serias, no solo tiene un número fijo de variables primitivas, sino que crea variables sobre la marcha en llamadas a funciones, o tiene variables de tamaño variable ... o algo así, como sea posible en un lenguaje completo de Turing. Eso hace que el espacio de estado sea de dimensión infinita, rompiendo todas las esperanzas de una cobertura total, incluso con equipos de prueba absurdamente poderosos.
Dicho eso ... en realidad las cosas no son tan sombrías. Que es posible proove programas enteros a ser correcta, pero usted tendrá que renunciar a algunas ideas.
Primero: es muy recomendable cambiar a un lenguaje declarativo. Los lenguajes imperativos, por alguna razón, siempre han sido de lejos los más populares, pero la forma en que combinan algoritmos con interacciones del mundo real hace que sea extremadamente difícil incluso decir lo que quiere decir con "correcto".
Mucho más fácil en lenguajes de programación puramente funcionales : tienen una clara distinción entre las propiedades realmente interesantes de las funciones matemáticas y las interacciones difusas del mundo real sobre 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 argumento) sale el resultado deseado correspondiente, entonces 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 de dimensión infinita. Es cierto, aunque para una sola función, ¡incluso las pruebas de cobertura ingenuas lo llevan más allá 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 teóricamente. No es fácil encontrar los tipos correctos que son realmente tan generales que puede probar esto completamente; 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 con polimorfismo paramétrico solo aumenta enormemente su "nivel de seguridad": no necesariamente encontrará todos los errores, ¡pero tendrá que ocultarlos bastante bien para que el compilador no los detecte!
fuente