¿La cobertura de ruta garantiza encontrar todos los errores?

64

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.


fuente
33
Esto es equivalente al problema de detención .
31
¿Qué pasa si el código que debería haber estado allí, no?
RemcoGerlich
66
@Snowman: No, no lo es. No es posible resolver el problema de detención para todos los programas, pero para muchos programas específicos es solucionable. Para estos programas, todas las rutas de código se pueden enumerar en una cantidad de tiempo finita (aunque posiblemente larga).
Jørgen Fogh
3
@ JørgenFogh Pero cuando se trata de encontrar errores en cualquier programa, ¿no se desconoce a priori si el programa se detiene o no? ¿No es esta pregunta sobre el método general de "encontrar todos los errores en cualquier programa a través de la cobertura de ruta"? En cuyo caso, ¿no es similar a "encontrar si algún programa se detiene"?
Andres F.
1
@AndresF. solo se desconoce si el programa se detiene si el subconjunto del lenguaje en el que está escrito es capaz de expresar un programa que no se detiene. Si su programa está escrito en C sin usar bucles ilimitados / recursividad / setjmp, etc., o en Coq, o en ESSL, entonces debe detenerse y se pueden rastrear todas las rutas. (La integridad de Turing está seriamente sobrevalorada)
Leushenko

Respuestas:

128

Si se prueba cada ruta a través de un programa, ¿eso garantiza encontrar todos los errores?

No

¿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?

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):

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

Han pasado dos décadas desde que se señaló que las pruebas del programa pueden demostrar de manera convincente la presencia de errores, pero nunca pueden demostrar su ausencia. Después de citar devotamente este comentario bien publicitado, el ingeniero de software vuelve al orden del día y continúa refinando sus estrategias de prueba, al igual que el alquimista de antaño, que continuó refinando sus purificaciones crioscósmicas.

- EW Dijkstra (énfasis agregado. Escrito en 1988. Han pasado considerablemente más de 2 décadas).

Mason Wheeler
fuente
77
@digitgopher: Supongo, pero si un programa no tiene entrada, ¿qué cosa útil hace?
Mason Wheeler
34
También existe la posibilidad de que falten pruebas de integración, errores en las pruebas, errores en las dependencias, errores en el sistema de compilación / implementación o errores en las especificaciones / requisitos originales. Nunca puede garantizar encontrar todos los errores.
Ixrec
11
@Ixrec: ¡Sin embargo, SQLite hace un esfuerzo bastante valiente! ¡Pero mira qué enorme esfuerzo es! Eso no se escalaría bien a grandes bases de código.
Mason Wheeler
13
No solo no probaría todos los valores posibles o combinaciones de los mismos, sino que no probó todos los tiempos relativos, algunos de los cuales podrían exponer las condiciones de carrera o hacer que su prueba entre en un punto muerto, lo que haría que no se informara nada . ¡Ni siquiera sería un fracaso!
Iwillnotexist Idonotexist
14
Mi recuerdo (respaldado por escritos como este ) es que Dijkstra creía que, en buenas prácticas de programación, la prueba de que un programa es correcto (en todas las condiciones) debería ser una parte integral del desarrollo del programa en primer lugar. Visto desde ese punto de vista, las pruebas son como la alquimia. En lugar de la hipérbole, creo que esta fue una opinión muy fuerte expresada en un lenguaje muy fuerte.
David K
71

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.

Jörg W Mittag
fuente
2
Podría asegurarse de que no haya ninguna excepción al llamar al código probado (con los parámetros en la prueba). Esto es un poco más que nada.
Paŭlo Ebermann
77
@ PaŭloEbermann De acuerdo, un poco más que nada. Sin embargo, es tremendamente menos que "encontrar todos los errores";)
Andres F.
1
@ PaŭloEbermann: Las excepciones son una ruta de código. Si el código se puede lanzar pero con ciertos datos de prueba no se lanza, la prueba no alcanza el 100% de cobertura de ruta. Esto no es específico de las excepciones como mecanismo de manejo de errores. Visual Basic ON ERROR GOTOtambién es un camino, como lo es C's if(errno).
MSalters
1
@MSalters Estoy hablando de código que (por especificación) no debería lanzar ninguna excepción, independientemente de la entrada. Si arroja alguno, eso sería un error. Por supuesto, si tiene un código que se especifica para lanzar una excepción, debe probarse. (Y, por supuesto, como dijo Jörg, simplemente verificar que el código no arroje una excepción generalmente no es suficiente para asegurarse de que haga lo correcto, incluso para el código que no arroja). Y algunas excepciones pueden ser lanzadas por un no - ruta de código visible, como para la desreferencia de puntero nulo o división por cero. ¿Tu herramienta de cobertura de ruta los atrapa?
Paŭlo Ebermann
2
Esta respuesta lo clava. Llevaría el reclamo aún más lejos y diría que debido a esto, la cobertura de ruta nunca garantiza encontrar ni un solo error. Sin embargo, existen métricas que pueden garantizar al menos que se detectarán cambios; las pruebas de mutación en realidad pueden garantizar que (algunas) modificaciones del código serán detectadas.
EIS
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 sortdevuelve 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.

Atsby
fuente
12

Considere la absfunción, 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
RemcoGerlich
fuente
2
Aquí hay otra implementación que pasa la prueba (perdone el Python sin interrupción de línea): def abs(x): if x == -3: return 3 else: return 0posiblemente podría eludir la else: return 0parte 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.
un CVn
7

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:

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

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:

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

Si su conjunto de pruebas puede generar todas las rutas para esto, felicidades, usted es un criptógrafo.

dureuill
fuente
Fácil para enteros suficientemente pequeños :)
CodesInChaos
Sin saber nada 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í, intpodría resultar un poco short.
dureuill
Con enteros de 32 bits y hashes criptográficos típicos (SHA2, SHA3, etc.), la computación debería ser bastante barata. Un par de segundos más o menos.
CodesInChaos
7

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:

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

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.

Jon Hanna
fuente
+1 Me gusta esta respuesta porque es constructiva y menciona algunos de los beneficios de la cobertura.
Andres F.
4

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

Pete Becker
fuente
1
Creo que eso depende de la definición de un error. No creo que las características o funcionalidades faltantes se consideren como errores.
EIS
@eis: ¿no ve un problema con un producto cuya documentación dice que hace X cuando en realidad no lo hace? Esa es una definición bastante limitada de "error". Cuando logré el control de calidad para la línea de productos C ++ de Borland, no fuimos tan generosos.
Pete Becker
No veo por qué documentación hace decir que si X que nunca se implementó
EIS
@eis: si el diseño original requería la característica X, la documentación podría terminar describiendo la característica X. Si nadie la implementó, eso es un error, y la cobertura de ruta (o cualquier otro tipo de prueba de caja negra) no lo encontrará.
Pete Becker
Vaya, la cobertura de ruta es una prueba de caja blanca , no una caja negra . Las pruebas de caja blanca no pueden detectar las características faltantes.
Pete Becker
4

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.

Derek W
fuente
Estuvo de acuerdo en que eso es parte del problema, pero el problema real es más fundamental que eso. Incluso con una computadora teórica con memoria infinita y sin concurrencia, el 100% de cobertura de prueba no implica la ausencia de errores. Ejemplos triviales de esto abundan en las respuestas aquí, pero aquí hay otro: si mi programa lo está times_two(x) = x + 2, esto estará completamente cubierto por el conjunto de pruebas assert(times_two(2) == 4), ¡pero esto obviamente es un código defectuoso! No hay necesidad de pérdidas de memoria :)
Andres F.
2
Es un gran punto y reconozco que es un clavo más grande / fundamental en el ataúd de la posibilidad de aplicaciones libres de errores, pero como usted dice que ya se agregó aquí y quería agregar algo que no estaba cubierto en respuestas existentes He oído hablar de aplicaciones que fallaron porque las conexiones de la base de datos no se liberaron nuevamente en el grupo de conexiones cuando ya no se necesitaban: una pérdida de memoria es solo el ejemplo canónico de la mala administración de los recursos. Mi punto era agregar que la gestión adecuada de los recursos en general no se puede probar por completo.
Derek W
Buen punto. Convenido.
Andres F.
3

Si se prueba cada ruta a través de un programa, ¿eso garantiza encontrar todos los errores?

Como ya se dijo, la respuesta es NO.

¿Si no, porque 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 las 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
BЈовић
fuente
2

¿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:

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 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:

  • 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.
  • Aún 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 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.

Nathan Long
fuente
0

Bueno ... , 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!

a la izquierda
fuente
No estoy de acuerdo con tu primera oración. Pasar por cada estado del programa no detecta, en sí mismo, ningún error. Incluso si busca bloqueos y errores explícitos, aún no ha verificado la funcionalidad real de ninguna manera, por lo que solo ha cubierto una pequeña parte del espacio de error.
Mateo leyó el
@MatthewRead: si aplica esto en consecuencia, entonces el "espacio de error" es un subespacio adecuado del espacio de todos los estados. Por supuesto, es hipotético porque incluso los estados "correctos" constituyen un espacio demasiado grande para permitir cualquier prueba exhaustiva.
Leftaroundabout