====== Testing Unitario ====== El software es una de las construcciones humanas más complejas, como se discutió en clases. Por lo tanto, es comprensible que los sistemas de software estén sujetos a los más diversos tipos de errores e inconsistencias. Para evitar que tales errores lleguen a los usuarios finales y causen perjuicios de valor incalculable, es fundamental introducir actividades de prueba en los proyectos de desarrollo de software. De hecho, las pruebas son una de las prácticas de programación más valoradas hoy en día, en cualquier tipo de software. También es una de las prácticas que ha sufrido más transformaciones en los últimos años. Cuando el desarrollo era en cascada, las pruebas se realizaban en una fase separada, después de las fases de recopilación de requisitos, análisis, diseño y codificación. Además, existía un equipo separado de pruebas, encargado de verificar si la implementación cumplía con los requisitos del sistema. Para comprobar esto, con frecuencia las pruebas eran manuales, es decir, una persona usaba el sistema, ingresaba datos y verificaba si las salidas eran las esperadas. Así, el objetivo de esas pruebas era solo detectar errores antes de que el sistema entrara en producción. Con los métodos ágiles, la práctica de las pruebas de software se reformuló profundamente, como explicamos a continuación: * Gran parte de las pruebas comenzó a ser automatizada, es decir, además de implementar las clases de un sistema, los desarrolladores comenzaron a implementar también código para probar esas clases. Así, los programas se volvieron auto-testables. * Las pruebas ya no se implementan después de que todas las clases de un sistema estén listas. Muchas veces, se implementan incluso antes de que esas clases estén terminadas. * Ya no existen grandes equipos de pruebas, o si existen, son responsables de pruebas específicas. En su lugar, el desarrollador que implementa una clase también debe implementar sus pruebas. * Las pruebas ya no son solo una herramienta exclusiva para detectar errores. Claro, esto sigue siendo importante, pero las pruebas han adquirido nuevas funciones, como verificar si una clase seguirá funcionando después de que se corrija un error en otra parte del sistema. Las pruebas también se utilizan como documentación para el código de producción. Estas transformaciones han convertido las pruebas en una de las prácticas de programación más valoradas en el desarrollo moderno de software. Es en este contexto que debemos entender la frase de Michael Feathers: //si un código no viene acompañado de pruebas, puede considerarse de baja calidad o incluso un código legado//. Esta sección se enfoca en las pruebas automatizadas, ya que las pruebas manuales requieren mucho trabajo, son lentas y caras. Peor aún, deben repetirse cada vez que el sistema sufra una modificación. Una forma interesante de clasificar las pruebas automatizadas es mediante una pirámide de pruebas (ver slides), originalmente propuesta por Mike Cohn [[https://dl.acm.org/doi/book/10.5555/1667109|link]]. Particularmente, las pruebas están divididas en tres grupos. Las pruebas unitarias verifican automáticamente pequeñas partes de un código, generalmente solo una clase (consulte también las figuras en la página siguiente). Estas pruebas forman la base de la pirámide, es decir, la mayor parte de las pruebas están en esta categoría. Las pruebas unitarias son simples, más fáciles de implementar y se ejecutan rápidamente. En el siguiente nivel, tenemos pruebas de integración o de servicios, que verifican una funcionalidad o transacción completa de un sistema. Por lo tanto, son pruebas que utilizan varias clases de diferentes paquetes y pueden también probar componentes externos, como bases de datos. Las pruebas de integración requieren más esfuerzo para implementarse y se ejecutan de forma más lenta. Finalmente, en la cima de la pirámide, tenemos las pruebas de sistema, también llamadas pruebas de interfaz de usuario. Estas simulan, de la forma más fiel posible, una sesión de uso del sistema por un usuario real. Como son pruebas de extremo a extremo (end-to-end), son más costosas, más lentas y menos numerosas. Las pruebas de interfaz suelen ser también frágiles; es decir, cambios mínimos en los componentes de la interfaz pueden requerir modificaciones en estas pruebas. Las pruebas unitarias son pruebas automatizadas de pequeñas unidades de código, normalmente clases, que se prueban de forma aislada del resto del sistema. Una prueba unitaria es un programa que llama métodos de una clase y verifica si devuelven los resultados esperados. Así, cuando se utilizan pruebas unitarias, el código de un sistema puede dividirse en dos grupos: un conjunto de clases, que implementan los requisitos del sistema, y un conjunto de pruebas. Las pruebas unitarias se implementan usando frameworks construidos específicamente para este fin. Los más conocidos se llaman frameworks xUnit, donde la "x" designa el lenguaje utilizado para implementar las pruebas. El primero de estos frameworks, llamado sUnit, fue implementado por Kent Beck a finales de la década de los 80 para Smalltalk. En esta sección, la pruebas serán implementadas en Java usando JUnit. La primera versión de JUnit fue implementada en conjunto por Kent Beck y Erich Gamma en 1997, durante un viaje en avión entre Suiza y EE.UU. Hoy en día, existen versiones de frameworks xUnit para los principales lenguajes. Por lo tanto, una de las ventajas de las pruebas unitarias es que los desarrolladores no necesitan aprender un nuevo lenguaje de programación, ya que las pruebas se implementan en el mismo lenguaje del sistema que se pretende probar. Para explicar los conceptos de pruebas unitarias, usaremos una clase Stack (Pila): import java.util.ArrayList; import java.util.EmptyStackException; public class Stack { private ArrayList elements = new ArrayList(); private int size = 0; public int size() { return size; } public boolean isEmpty() { return (size == 0); } public void push(T elem) { elements.add(elem); size++; } public T pop() throws EmptyStackException { if (isEmpty()) throw new EmptyStackException(); T elem = elements.remove(size-1); size--; return elem; } } JUnit permite implementar clases que probarán — de forma automática — las clases de la aplicación, como la clase Stack. Por convención, las clases de prueba tienen el mismo nombre que las clases que se prueban, pero con el sufijo Test. Por lo tanto, nuestra primera clase de prueba se llamará StackTest. Los métodos de prueba comienzan con el prefijo test y deben cumplir obligatoriamente con las siguientes condiciones: (1) ser públicos, ya que serán llamados por JUnit; (2) no tener parámetros; (3) tener la anotación @Test, la cual identifica los métodos que deben ejecutarse durante una prueba. A continuación, mostramos nuestra primera prueba unitaria: import org.junit.Test; import static org.junit.Assert.assertTrue; public class StackTest { @Test public void testEmptyStack() { Stack stack = new Stack(); boolean empty = stack.isEmpty(); assertTrue(empty); } } En esta primera versión, la clase StackTest tiene un único método de prueba, público, anotado con ''@Test'' y llamado ''testEmptyStack()''. Este método solo crea una pila y verifica si está vacía. Los métodos de prueba tienen la siguiente estructura: * Primero, se crea el contexto de la prueba, también llamado fixture. Para ello, se deben instanciar los objetos que se van a probar y, si es necesario, inicializarlos. En nuestro primer ejemplo, esta parte de la prueba incluye solo la creación de una pila llamada stack. * A continuación, la prueba debe llamar a uno de los métodos de la clase que se está probando. En el ejemplo, llamamos al método isEmpty() y almacenamos su resultado en una variable local. * Finalmente, debemos comprobar si el resultado del método es el esperado. Para ello, se utiliza un comando llamado assert. De hecho, JUnit ofrece varias variantes de assert, pero todas tienen el mismo objetivo: comprobar si un determinado resultado es igual a un valor esperado. En el ejemplo, usamos assertTrue, que verifica si el valor pasado como parámetro es verdadero. Las IDEs ofrecen opciones para ejecutar solo las pruebas de un sistema, por ejemplo, a través de una opción de menú llamada Run as Test. Es decir, si el desarrollador selecciona Run, ejecutará su programa normalmente, comenzando por el método main. Sin embargo, si elige la opción Run as Test, no ejecutará el programa, sino solo sus pruebas unitarias.