miércoles, 8 de diciembre de 2010

Prueba Unitaria (Unit Testing) con Netbeans

¿Cómo pruebo mis programas de Estructura de Datos? ¿Existe algo mejor que imprimir resultados en la consola y analizar visualmente si el programa funcionó bien?

Por supuesto, desarrollando pruebas unitarias para tu código aseguras que tu implementación se comporta como esperas y mejor aún, si realizas algún cambio en el código, puedes correr tu prueba nuevamente para validar que todo siga funcionando correctamente. En resumen, una prueba unitaria es código que evalúa partes específicas de otro programa.

Suena como trabajo doble ¿Aparte de mi implementación tengo que hacer otro programa para probarlo?

Realmente no, ya que existen librerías que te ayudan a desarrollar las pruebas rápidamente. Es más, IDE´s como Netbeans incluyen atajos y opciones para generar y ejecutar las pruebas. La siguiente imagen muestra cómo el proyecto por default de Neatbeans ya incluye folders y librerías para desarrollar pruebas.

¿Cómo puedo probar una lista que tiene como tipo de datos enteros e inserta los valores en orden?

Supongamos que los métodos iniciales de la lista son los siguientes:

public class ListaDoble {
   public void insertar(int n){...}
   public NodoDoble remover(int valor){...}
   public boolean isEmpty(){...}
   public static void main(String args[]){...}
}

Primero, borrar el método main, ya que eso está fuera de las responsabilidades de la clase. Vamos hacer dos cambios a la clase:

  1. Agregar una variable privada que mantenga el número de elementos en la lista: numElementos. Se incrementará cuando se inserte un elemento y se drecrementará en el método remover.
  2. Adicionar un método que nos retorne la lista como un arreglo de enteros. Esta es una posible implementación:
        public int[] toArray(){
           int[] array = new int[numElementos];
           int i = 0;
           NodoDoble auxiliar = inicio;
           while (auxiliar != null) {
               array[i] = auxiliar.getDatos();
               auxiliar = auxiliar.getSiguienteNodo();
               i++;
           }
           return array;
       }

Ahora veamos lo rápido y fácil que es agregar una prueba.

  1. Da clic derecho en la clase que queremos probar, selecciona la opcion Tools (Herramientas) > Create JUnit Tests
  2. Aparece un diálogo con opciones para crear la clase que ejecutará las pruebas. Para esta prueba seleccioné lo mínimo, como puedes ver en la figura.
  3. Borra el método por default que genera Netbeans y agrega el siguiente método.
        @Test
       public void testEscenario01() {
           ListaDoble lista = new ListaDoble();
           // La lista debe estar vacia al inicio
           assertTrue(lista.isEmpty());
    
           // Insertemos 5 elementos
           lista.insertar(100);
           lista.insertar(50);
           lista.insertar(200);
           lista.insertar(-50);
           lista.insertar(0);
           assertArrayEquals(lista.toArray(),
                   new int[]{-50,0,50,100,200});
       }
    Como puedes ver, este método crea una instancia de ListaDoble. Los métodos assert son parte de la librería JUnit, y sirven para verificar si cierta condición se cumple. Por ejemplo, recién creada la lista, se espera que esté vacía, esto lo logramos con assertTrue(lista.isEmpty()). Después insertamos 5 elementos y verificamos con assertArrayEquals que el contenido de la lista sea igual al arreglo que pasamos como segundo parámetro. Nota que el segundo arreglo está ordenado, ya que es lo que esperamos de la lista.
  4. Para ejecutar la prueba, simplemente da clic derecho en el archivo de prueba (en este caso ListaDobleTest) y selecciona Test File
    Si tu implementación es correcta, aparecerá un mensaje como el siguiente.
    Para la siguiente imagen, introduje un error en la prueba para ver la salida de JUnit.

NOTA

Esto nos lleva a reflexionar, que pueden existir errores tanto en la implementación como en la prueba. Pero es importante notar que sin pruebas confiamos ciegamente en la implementación. Si hay un error al correr las pruebas, el error puede estar en la implementación o en las pruebas. Si las pruebas corrieron exitosamente, entonces la implementación cumple con los casos presentados en las pruebas; en otras palabras, puede que tanto la implementación como las pruebas estén mal. Obviamente, es más difícil equivocarse en dos lugares (implementación y pruebas) que sólo en uno (implementación).

También es importante mencionar que las pruebas sólo garantizan que nuestro programa cumple con los escenarios mencionados, más no que está 100% libre de errores.

¿Con una sola prueba basta para la clase ListaDoble?

El objetivo no es crear tantas pruebas como sea posible. Más bien, es crear el mínimo número de pruebas que cubran el mayor número de casos o escenarios de nuestra implementación. Por ejemplo, supongamos que en la prueba uno insertamos 20,19, 18, 17, 16 y en la prueba dos insertamos 10,9,8,7,6. Realmente, en las dos pruebas estamos insertando elementos en orden descendente; es decir, las dos cubren el mismo escenario (son redundantes).

Con esto en mente, recomiendo crear escenarios que se complementen. Para el caso de la inserción en la lista doble: 1) insertar elementos en order ascendente, 2) insertar elementos en orden descendente, 3) insertar elementos tal que un elemento se inserte al inicio de la lista, el siguiente elemento al final de lista, y otro elemento en un punto medio de la lista y así sucesivamente, 4) insertar sólo elementos iguales.

Observa que estos casos son para la inserción, casos similares deben ser creados para el borrado. Y por último recomendaría un conjunto de casos que mezclaran operaciones de inserción y borrado.

Veo que al comparar la lista en forma de arreglo usamos un arreglo que nosotros calculamos new int[]{-50,0,50,100,200}, ¿cómo difiere esto de imprimirlo en la consola e inspeccionarlo visualmente?

Quizá las primeras dos o tres veces de inspeccionar el resultado visualmente resulte práctico, pero después de un buen rato de estar desarrollando verás que resulta demasiado tedioso y propenso a errores. Imagina que tienes una lista de 100 números, no suena muy divertido inspeccionar visualmente los 100 números cada vez que realices un cambio a la lista.

Además, como menciono en el punto anterior, es necesario probar varios escenarios. Si creas tus pruebas unitarias, entonces cada vez que las ejecutes estarás evaluando todos los escenarios en un abrir y cerrar de ojos. Esto te ayuda a tener más confianza al realizar cambios o mejoras en tu implementación, ya que puedes ir probando continuamente para validar si algo deja de funcionar o si algo ya está funcionando.

No hay comentarios:

Publicar un comentario