unit testing - unitarias - ¿Qué tipo de errores podría contener mi código incluso si tengo una cobertura de código del 100%?
tdd ejemplo (21)
funciona en mi máquina
Muchas cosas funcionan bien en la máquina local y no podemos asegurar que trabaje en puesta en escena / producción. Code Coverage puede no cubrir esto.
¿Qué tipo de errores podría contener mi código incluso si tengo una cobertura de código del 100%? Estoy buscando ejemplos concretos o enlaces a ejemplos concretos de tales errores.
¿Uh? Cualquier tipo de error lógico ordinario, supongo? La corrupción de la memoria, el desbordamiento del búfer, el código erróneo anterior simple, la asignación en lugar de la prueba, la lista continúa. La cobertura es solo eso, le permite saber que todas las rutas de código se ejecutan, no que son correctas.
Argumento de validación, aka. Null Checks. Si toma cualquier entrada externa y la pasa a funciones pero nunca verifica si son válidas / nulas, entonces puede lograr una cobertura del 100%, pero igual obtendrá una NullReferenceException si de alguna manera pasa nulo a la función porque eso es lo que da su base de datos tú.
también, desbordamiento aritmético, como
int result = int.MAXVALUE + int.MAXVALUE;
La Cobertura del código solo cubre el código existente, no podrá indicarle dónde debe agregar más código.
Bueno, si tus pruebas no prueban lo que sucede en el código cubierto. Si tiene este método que agrega un número a las propiedades, por ejemplo:
public void AddTo(int i)
{
NumberA += i;
NumberB -= i;
}
Si su prueba solo verifica la propiedad NumberA, pero no la NumberB, tendrá una cobertura del 100%, la prueba se aprobará, pero NumberB aún contendrá un error.
Conclusión: una prueba unitaria con 100% no garantizará que el código esté libre de errores.
Code Coverage no significa mucho. Lo que importa es si todos (o la mayoría) de los valores argumentales que afectan el comportamiento están cubiertos.
Por ejemplo, considere un método típico compareTo (en java, pero se aplica en la mayoría de los idiomas):
//Return Negative, 0 or positive depending on x is <, = or > y
int compareTo(int x, int y) {
return x-y;
}
Siempre que tenga una prueba para compareTo(0,0)
, obtendrá cobertura de código. Sin embargo, necesita al menos 3 testcases aquí (para los 3 resultados). Aún así no está libre de errores. También vale la pena agregar pruebas para cubrir condiciones excepcionales / de error. En el caso anterior, si prueba compareTo(10, Integer.MAX_INT)
, va a fallar.
En resumen: intente dividir su entrada para separar conjuntos en función del comportamiento; haga una prueba para al menos una entrada de cada conjunto. Esto agregará más cobertura en el verdadero sentido.
También busque herramientas como QuickCheck (si está disponible para su idioma).
Como se menciona en muchas de las respuestas aquí, puede tener una cobertura de código del 100% y aún así tener errores.
Además de eso, puede tener 0 errores pero la lógica en su código puede ser incorrecta (no cumple los requisitos). La cobertura del código, o estar 100% libre de errores no puede ayudarle con eso en absoluto.
Una práctica típica de desarrollo de software corporativo podría ser la siguiente:
- Tener una especificación funcional claramente escrita
- Tener un plan de prueba escrito contra (1) y someterlo a revisión por pares
- Tener casos de prueba escritos contra (2) y hacer que sean revisados por pares
- Escribir código en contra de la especificación funcional y hacer que sea revisada por pares
- Pon a prueba tu código contra los casos de prueba
- Haga un análisis de cobertura de código y escriba más casos de prueba para lograr una buena cobertura.
Tenga en cuenta que dije "bueno" y no "100%". Es posible que no siempre sea posible alcanzar el 100% de cobertura; en ese caso, la mejor manera de gastar su energía es en lograr la exactitud del código, en lugar de la cobertura de algunas ramas oscuras. Diferentes tipos de cosas pueden salir mal en cualquiera de los pasos 1 a 5 anteriores: idea incorrecta, especificación incorrecta, pruebas incorrectas, código incorrecto, ejecución de prueba incorrecta ... La conclusión es que el paso 6 por sí solo no es el paso más importante en el proceso.
Ejemplo concreto de código incorrecto que no tiene ningún error y tiene una cobertura del 100%:
/**
* Returns the duration in milliseconds
*/
int getDuration() {
return end - start;
}
// test:
start = 0;
end = 1;
assertEquals(1, getDuration()); // yay!
// but the requirement was:
// The interface should have a method for returning the duration in *seconds*.
Como todavía no lo he visto mencionado, me gustaría añadir este hilo de que la cobertura del código no le dice qué parte de su código está libre de errores.
Solo le dice qué partes de su código están garantizadas para ser no probadas.
Considera el siguiente código:
int add(int a, int b)
{
return a + b;
}
Este código podría no implementar algunas funcionalidades necesarias (es decir, no cumplir con los requisitos del usuario final): "100% de cobertura" no necesariamente prueba / detecta la funcionalidad que debería implementarse pero que no lo es.
Este código podría funcionar para algunos, pero no para todos los rangos de datos de entrada (por ejemplo, cuando a y b son ambos muy grandes).
En un reciente documento de IEEE Software "Dos errores y software libre de errores: una confesión", Robert Glass argumentó que en el "mundo real" hay más errores causados por lo que él llama lógica o combinatoria faltante (que no se puede proteger contra con herramientas de cobertura de código) que por errores de lógica (que pueden).
En otras palabras, incluso con una cobertura de código del 100%, aún corre el riesgo de encontrar este tipo de errores. Y lo mejor que puede hacer es, lo adivinó, hacer más revisiones de código.
La referencia al documento está aquí y encontré un resumen aproximado aquí .
Errores en las pruebas :)
La cobertura del código no significa nada, si sus pruebas contienen errores, o si está probando algo incorrecto.
Como una tangente relacionada; Me gustaría recordarle que puedo construir trivialmente un método O (1) que satisfaga la siguiente prueba de pseudocódigo:
sorted = sort(2,1,6,4,3,1,6,2);
for element in sorted {
if (is_defined(previousElement)) {
assert(element >= previousElement);
}
previousElement = element;
}
karma de bonificación para Jon Skeet, quien señaló la laguna en la que estaba pensando
La cobertura del código no significa que su código esté libre de errores de ninguna manera. Es una estimación de cuán bien sus casos de prueba cubren su base de código fuente. La cobertura del código del 100% implicaría que cada línea de código se prueba, pero ciertamente no todos los estados de su programa. Se están llevando a cabo investigaciones en esta área, creo que se conoce como modelado de estados finitos, pero realmente es una forma de fuerza bruta de tratar de explorar cada estado de un programa.
Una forma más elegante de hacer lo mismo es algo que se conoce como interpretación abstracta. MSR (Microsoft Research) lanzó algo llamado CodeContracts basado en la interpretación abstracta. Echa un vistazo a Pex también, realmente hacen hincapié en los métodos de prueba de comportamiento de tiempo de ejecución de la aplicación .
Podría escribir una muy buena prueba que me diera una buena cobertura, pero no hay garantías de que esa prueba explore todos los estados que mi programa podría tener. Este es el problema de escribir pruebas realmente buenas, lo cual es difícil.
La cobertura del código no implica buenas pruebas
No sé de nadie más, pero no llegamos a ninguna cobertura cercana al 100%. Ninguno de nuestros "Esto nunca debería suceder". Las capturas se ejercitan en nuestras pruebas (bueno, a veces lo hacen, pero luego se corrige el código, ¡así que ya no lo hacen!). Me temo que no me preocupa que pueda haber un error de sintaxis / lógica en un nunca-sucede-CATCH
Para su información, Microsoft Pex intenta ayudar explorando su código y encontrando casos "extremos", como dividir por cero, desbordamiento, etc.
Esta herramienta es parte de VS2010, aunque puede instalar una versión de vista previa de tecnología en VS2008. Es bastante notable que la herramienta encuentre lo que encuentra, sin embargo, IME, todavía no te llevará hasta "a prueba de balas".
Por lo general, la cobertura de código solo le indica cuántas de las ramas de una función están cubiertas. Por lo general, no informa las diversas rutas que pueden tomarse entre las llamadas a funciones . Se producen muchos errores en los programas porque la transferencia de un método a otro es incorrecta, no porque los métodos mismos contengan errores. Todos los errores de esta forma todavía podrían existir en una cobertura de código del 100%.
Realice una cobertura de código del 100%, es decir, 100% de instrucciones, 100% de dominios de entrada y salida, 100% de rutas, 100% cualquier cosa que piense, y aún puede tener errores en su código: características faltantes .
Siempre puede haber excepciones de tiempo de ejecución: relleno de memoria, base de datos u otras conexiones que no se cierran, etc.
Su producto podría ser técnicamente correcto, pero no cumplir las necesidades del cliente.
1. Problemas de "espacio de datos"
Tu (mal) código:
void f(int n, int increment)
{
while(n < 500)
{
cout << n;
n += increment;
}
}
Su prueba:
f(200,100);
Error en el uso del mundo real:
f(200,0);
Mi punto: su prueba puede cubrir el 100% de las líneas de su código, pero no cubrirá (típicamente) todos sus posibles espacios de datos de entrada, es decir, el conjunto de todos los valores posibles de las entradas.
2. Probando contra tu propio error
Otro ejemplo clásico es cuando solo tomas una mala decisión en diseño y prueba tu código contra tu propia mala decisión.
Por ejemplo, el documento de especificaciones dice "imprimir todos los números primos hasta n " e imprime todos los números primos hasta n pero excluyendo n . Y las pruebas de tu unidad prueban tu idea equivocada.
3. Comportamiento indefinido
Use el valor de las variables no inicializadas, cause un acceso no válido a la memoria, etc. y su código tenga un comportamiento indefinido (en C ++ o en cualquier otro idioma que contemple "comportamiento indefinido"). A veces pasará sus pruebas, pero se bloqueará en el mundo real.
...
Tener una cobertura de código del 100% no es tan bueno como uno pueda pensar. Considera un ejemplo trival:
double Foo(double a, double b)
{
return a / b;
}
Incluso una prueba de unidad única elevará la cobertura de código de este método al 100%, pero dicha prueba unitaria no nos dirá qué código está funcionando y qué código no lo está. Este podría ser un código perfectamente válido, pero sin probar las condiciones de borde (como cuando b
es 0.0
) la prueba unitaria no es concluyente en el mejor de los casos.
La cobertura del código solo nos dice qué fue ejecutado por nuestras pruebas unitarias, no si se ejecutó correctamente. Esta es una distinción importante para hacer. El hecho de que una línea de código se ejecute mediante una prueba unitaria no significa necesariamente que esa línea de código funcione según lo previsto.
Escucha esto para una discusión interesante.
Casi cualquier cosa.
¿Has leído Code Complete ? (Debido a que dice que realmente debería hacerlo ). En el Capítulo 22 dice "La cobertura de enunciados al 100% es un buen comienzo, pero es apenas suficiente". El resto del capítulo explica cómo determinar qué pruebas adicionales agregar. Aquí hay una breve prueba.
Las pruebas estructuradas y las pruebas de flujo de datos significan probar cada ruta lógica a través del programa. Hay cuatro rutas a través del código artificial a continuación, dependiendo de los valores de A y B. La cobertura de enunciados del 100% podría lograrse probando solo dos de las cuatro rutas, quizás
f=1:g=1/f
f=0:g=f+1
. Perof=0:g=1/f
dará una división por error cero. Debe tener en cuenta si las sentencias while y for para loops (el cuerpo del ciclo nunca se puede ejecutar) y cada rama de una instrucción select o switch .If A Then
f = 1
Else
f = 0
End If
If B Then
g = f + 1
Else
g = f / 0
End If
Adivinación de errores : conjeturas informadas sobre los tipos de entrada que a menudo causan errores. Por ejemplo, condiciones de contorno (desactivado por errores), datos no válidos, valores muy grandes, valores muy pequeños, ceros, nulos, colecciones vacías.
Y aun así puede haber errores en sus requisitos, errores en sus pruebas, etc., como han mencionado otros.