¿Cuáles son algunos consejos importantes para detectar / cubrir casos extremos al codificar soluciones?

Hay un concepto importante en las pruebas de software llamado clases de equivalencia . La idea detrás es dividir los datos de entrada en clases para que cada clase de equivalencia le proporcione una nueva prueba unitaria. En la práctica, eso significa que si sospecha fuertemente que dos entradas para la función no proporcionarían información adicional o cobertura de código y son, en cierto sentido, intercambiables, entonces puede usar solo una de ellas.

Usemos un ejemplo con una función que toma dos enteros y los suma. Parece ser bastante simple de probar, y generalmente un desarrollador no pasaría mucho tiempo en eso. Pero intentemos cubrir todas las clases de equivalencia posibles aquí. En aras de la simplicidad, escribiré sobre una función Java con números enteros que sean números entre -2 ^ 31 y 2 ^ 31 – 1.

public int sumOfTwoIntegers (int a, int b) {return a + b;}

Entonces aquí está nuestra función. Ahora escribamos qué tipos de entradas podemos suministrar aquí. Primero, dividimos nuestras entradas en rangos correctos e incorrectos. Como nuestra función nos proporciona dos enteros como inpust y también devolvemos un entero, tenemos que lidiar con el desbordamiento, en ambos lados del rango de valores enteros. Entonces, las entradas correctas serían dos enteros cuya suma se encuentra dentro del rango de -2 ^ 31 y 2 ^ 31 – 1. Ahora, podemos agregar pruebas que nos mostrarán cómo se comportará nuestra función, cuando estemos fuera de este rango :

sumOfTwoIntegers (Integer.MAX_VALUE, 50); // Vamos a desbordarlo a la derecha
sumOfTwoIntegers (50, Integer.MAX_VALUE); // Voltear los argumentos – caso de prueba separado

sumOfTwoIntegers (Integer.MIN_VALUE, -50); // Ahora a la izquierda
sumOfTwoIntegers (-50, Integer.MIN_VALUE); // Y voltéalos de nuevo

Muy bien, eso parece cubrir los desbordamientos definitivos en ambos lados. Ahora, acerquémonos a los límites de los valores enteros y hagamos una pregunta, ¿qué pasa si nos desbordamos un poco, por uno? ¿Será diferente? Posiblemente. Así que vamos otra vez, pero más cerca de los límites.

sumOfTwoIntegers (Integer.MAX_VALUE, 1); // Vamos a desbordarlo un poco
sumOfTwoIntegers (1, Integer.MAX_VALUE); // Voltea los args nuevamente

sumOfTwoIntegers (Integer.MIN_VALUE, -1); // Ahora a la izquierda
sumOfTwoIntegers (-1, Integer.MIN_VALUE); // Y voltéalos de nuevo

Ok, ahora llegamos a los límites de nuestra entrada. Los límites nos presentan otros cuatro casos de prueba:

sumOfTwoIntegers (Integer.MAX_VALUE – 1, 1); // Ahora llegamos al límite
sumOfTwoIntegers (1, Integer.MAX_VALUE – 1); // Voltea los args nuevamente

sumOfTwoIntegers (Integer.MIN_VALUE + 1, -1); // Ahora a la izquierda
sumOfTwoIntegers (-1, Integer.MIN_VALUE + 1); // Y voltéalos de nuevo

Ok, parece que hemos terminado con los límites por ahora. Pasemos a las entradas comunes que esperaría usar con esta función. No está de más tener dos casos de prueba separados, incluso si todos nuestros valores parecen estar en la misma clase de equivalencia, ya que no hay garantía de que realmente se comporten de la misma manera:

sumOfTwoIntegers (5, 1); // Dos números positivos
sumOfTwoIntegers (1, 5); // Conmutado
sumOfTwoIntegers (78, 23); // Otro par
sumOfTwoIntegers (23, 78); // Conmutado

sumOfTwoIntegers (-5, -1); // Dos números negativos
sumOfTwoIntegers (-1, -5); // Conmutado
sumOfTwoIntegers (-78, -23); // Otro par
sumOfTwoIntegers (-23, -78); // Conmutado

sumOfTwoIntegers (-5, 1); // números positivos y negativos
sumOfTwoIntegers (1, -5); // Conmutado
sumOfTwoIntegers (-78, 23); // Otro par
sumOfTwoIntegers (23, -78); // Conmutado

En realidad, hasta este punto, tratamos con argumentos positivos o negativos, pero también hay un cero que puede estropearlo todo. ¿Qué pasa si tratamos de permanecer en el límite con cero como argumento? ¿Cómo influye en otras entradas?

sumOfTwoIntegers (6, 0); // Positivo y cero
sumOfTwoIntegers (0, 6); // Conmutado

sumOfTwoIntegers (-8, 0); // Negativo y cero
sumOfTwoIntegers (0, -8); // Conmutado

sumOfTwoIntegers (0, 0); // Dos ceros

// Límites con ceros
sumOfTwoIntegers (Integer.MAX_VALUE, 0);
sumOfTwoIntegers (0, Integer.MAX_VALUE);

sumOfTwoIntegers (Integer.MIN_VALUE, 0);
sumOfTwoIntegers (0, Integer.MIN_VALUE);

Bueno, ahora parece que lo cubrimos bastante bien. Entonces, ¿cuántos casos de prueba necesitábamos para cubrir diferentes casos límite para una suma de dos enteros? Un enorme 34 prueba una función con dos entradas de un tipo primitivo que tiene una sola línea de código.

¿Entonces, qué podemos aprender de esto? Primero, nunca puede tener suficientes pruebas para demostrar que su código es “correcto”, ya que no es factible. Simplemente refuta que no funciona para algún subconjunto de las entradas, que deberían representar las clases de equivalencia. En segundo lugar, antes de escribir pruebas unitarias, un desarrollador debe prestar mucha atención a qué tipos de entradas tiene la función y qué combinaciones posibles de ellas proporcionan información adicional sobre cómo las maneja el código. Algunos ejemplos:

  • Si es un número, ¿puede causar un desbordamiento? Tal vez la división por cero? ¿Puede ser un número de coma flotante? ¿Puede haber errores debido al redondeo arriba / abajo durante el cálculo?
  • Si es una cuerda, ¿cuánto puede durar? Puede estar vacio? ¿Qué pasa si la codificación de la cadena no es estándar?
  • Si es un objeto, ¿puede ser nulo? ¿Qué podemos cambiar en el estado del objeto?

Emplee la mentalidad de alguien que quiera romper y / o explotar activamente el sistema, primero imagine que es una caja negra e intente todo tipo de combinaciones de entrada, luego mire el código dentro de la función para obtener una idea de lo que hace con la entrada . Intente causar tanto daño en la etapa de prueba, porque alguien puede intentar causarlo más tarde y el código debe manejarlo. Siempre pruebe su código, es esencial y la falta de pruebas adecuadas puede tener consecuencias desastrosas.

Enlaces:

  • Particionamiento de equivalencia – Wikipedia
  • Prueba de clase de equivalencia versus prueba de valor límite
  • Ingeniería de software de Dewsoft

Mira las condiciones (obviamente). Cualquier comparación de orden (“>”, ”<”, ”> =”, ”<=”) le solicita 3 valores que están cerca de borde y borde.

Mira el código manipulando colecciones. Los casos de borde son:

  • Vacío
  • Un item
  • Enorme

Si tiene objetos compuestos y su soporte de idioma (tengo en cuenta lo que viene a continuación) nulos, cada campo es nulo es un caso límite.

Un ejemplo simple en Java es una función “entre”. Puedo mostrarle un buggy entre funciones: (IDE en línea gratuito y Terminal)

import java.lang.Math;

clase pública HelloWorld
{
public static void main (String [] args)
{
para (int i = -10; i <15; i ++) {
if (isBetween (i, -6,10)) {
System.out.println (“” + i + “: sí”);
}más{
System.out.println (“” + i + “: no”);
}
}

}

public static boolean isBetween (int valor, int min, int max) {
return Math.abs (valor- (max + min) / 2) <= Math.abs ((max-min) / 2);
}
}

El código en la línea 18 no usa bucles, ¡pero contiene 2 errores!

¡Intenta encontrarlos antes de continuar!

El bucle en general es bastante convincente. Da resultados correctos. Aunque hay 2 errores.

  • La división se realiza en enteros. Hay un problema de redondeo que se muestra si reemplaza “-6” (línea 8) por “-7”.
  • Si “value- (max + min) / 2” es -2147483648 (Integer.MIN_VALUE), la expresión “Math.abs (value- (max + min) / 2)” se evaluará a -2147483648. Está documentado: Matemáticas (Java Platform SE 7)

Entonces, los desbordamientos y las peculiaridades de la biblioteca también son un buen lugar para buscar.

En el caso de un desarrollador de Java (Kotlin por extensión), este libro es imprescindible: Java Puzzlers

¡En realidad, esta es una especialidad para detectar tales errores!