Visión general
A pesar de que el testing automatizado no suele aplicarse, ya sea por falta de presupuesto o por plazos de entrega o porque se vea como una carga de trabajo opcional o incluso innecesaria, es parte fundamental en el proceso de creación y mantenimiento de cualquier desarrollo de software. Este documento explica en qué se basa el testing automatizado, en qué consisten las pruebas unitarias e introduce de forma práctica a la herramienta ideal para realizar tests unitarios en Java, JUnit.
¿Qué es el testing automatizado?
Un proyecto de software de cierta envergadura tiene implicado un equipo de desarrollo así como múltiples dependencias de otros proyectos y librerías. No es difícil que se vuelva insostenible probar que un pequeño cambio no produce errores en otra parte del código. En estos casos habría que probar manualmente toda la aplicación para saber si todo sigue bien, y aún así pueden haber condiciones que se pasen por alto y produzcan un error en el momento menos esperado.
Al escribir código que comprueba nuestro propio código estamos automatizando el proceso de testing. Como programadores, con cada compilación podremos lanzar un conjunto de pruebas básicas, llamadas test unitarios, para comprobar que los cambios introducidos no afectan al comportamiento esperado del método bajo los casos de prueba definidos, como si actuara independientemente al resto.
Con las pruebas automatizadas también se puede reducir sustancialmente el tiempo dedicado al siguiente nivel de pruebas, más costosas y complejas, los test de integración con otros módulos o sistemas externos. Además al automatizar aquellas partes que no requieren de inteligencia humana, los testers pueden dedicar mayor tiempo a pruebas críticas y situaciones complejas dejando lo simple y básico a las pruebas automatizadas.
Ventajas del testing automatizado
Algunas ventajas del testing automatizado:
- Fiabilidad: Lo más evidente es que permite la detección temprana e inmediata de errores. Las pruebas de regresión aumentan la fiabilidad del software cuando se producen cambios en el código. Esto permite avanzar en la implementación de un proyecto o en la inclusión de nuevas funcionalidades asegurando que estos cambios no rompen funcionalidades ya existentes que funcionan correctamente.
- Rápidez: Una vez implementadas, las pruebas automatizadas se pueden ejecutar todas las veces que sean necesarias sin ningún coste adicional en comparación con las pruebas realizadas manualmente. Esto no implica que no se tenga que realizar testing manual sino que este se invierta en aquellas pruebas en las que aporte verdadero valor.
- Mantenimiento: Obliga a que el código de la aplicación sea razonablemente fácil de testear lo que implica el seguimiento de buenas prácticas como los principios SOLID. Este efecto colateral ayuda a aumentar la calidad del código facilitando su mantenimiento.
Terminología
Precondición: También conocido como test fixture. Es una condición estable que se utiliza como parámetro para realizar una prueba. Por ejemplo, podría ser una cadena de texto con la cual validamos si el método bajo prueba se comporta tal como esperamos.
Test unitario: Fragmento de código escrito para probar y validar una funcionalidad específica, comportamiento o estado del código bajo prueba. Un test unitario aplica a pequeñas unidades de código, por ejemplo un método o clase. Cualquier dependencia externa debe ser eliminada o reemplazada por un objeto simulado (mock). Los test unitarios no deben probar interfaces de usuario complejas o interacciones con otros componentes. Para esto se desarrollan los test de integración.
Test de integración: También conocido como prueba funcional. Tiene como objetivo probar el comportamiento de un componente o la integración entre un conjunto de componentes. Las pruebas de integración comprueban que todo el sistema funciona según lo previsto, por lo que reducen la necesidad de pruebas manuales intensivas.
Cobertura: Es el porcentaje del código cubierto por los tests unitarios.
Mock: Se conocen como mock a los objetos que imitan el comportamiento de los objetos reales que son reemplazados. Sirve para probar nuestro código sin tener que hacer uso de componentes externos, que pueden que no estén disponibles o sean costosos de inicializar. Se le indica la respuesta esperada cuando reciba unos parámetros predeterminados.
¿Qué partes deben de ser probadas?
Lo ideal sería tener una cobertura del cien por cien, pero escribir pruebas para partes de código triviales es una pérdida de tiempo. Por ejemplo, es poco efectivo escribir pruebas para los métodos getter y setter.
Se deben escribir test para las partes críticas y complejas de la aplicación. Si en un futuro se introducen nuevas características, el conjunto de test servirá para detectar posibles errores y protegerá contra la regresión en el código existente.
Si se comienza a desarrollar los test para código ya existente, sin ninguna duda partiremos a escribir los test para aquellas partes del código donde se hubiese producido la mayor cantidad de errores en el pasado.
Como vemos, en cualquier caso, es una buena práctica enfocarse en las partes críticas de la aplicación.
¿Por qué hacer el testing con JUnit?
JUnit es la herramienta de testing automatizado más popular para Java, seguido de TestNG. Se encuentra completamente integrado en los IDEs más utilizados, por ejemplo Eclipse. JUnit puede utilizarse para cualquier tipo de testing automatizado y no solo para pruebas unitarias.
Proporciona la estructura base sobre la cual implementar los tests, en la cual se apoyan otras herramientas más complejas, como puede ser Mockito o JMockit, una herramienta mucho más completa ya analizada en este blog.
Los test de JUnit se implementan muy fácilmente. Se utilizan clases POJO, normalmente marcadas con el sufijo «Test». Cada método de prueba se anota con @Test
. En el más que probable caso de que nuestro proyecto se haya generado con Maven, las clases de prueba se encontrarán en la ruta /src/test/java y los recursos específicos para las pruebas en /src/test/resources. Esto nos permite separar el código de nuestro desarrollo de aquel específico de pruebas.
Como hemos visto, como desarrolladores, generalmente vamos centrarnos en la implementación de pruebas unitarias que suelen suponer el grueso de los tests automatizados de una aplicación. Aquí lo que probamos son unidades mínimas de código, generalmente métodos muy concretos, de forma exhaustiva y totalmente independiente del resto de la aplicación. JUnit nos facilitará enormemente esta labor.
Introducción al testing con Junit 4
El framework de JUnit provee al usuario de herramientas, clases y métodos que le facilitan la tarea de realizar pruebas en su sistema y así asegurar su consistencia y funcionalidad.
En este tutorial vamos a enseñar el funcionamiento básico de JUnit, en su versión 4, y su integración con el entorno de desarrollo Eclipse. A pesar de que recientemente se ha liberado la versión 5, a día de hoy consideramos que la versión anterior es mucho más práctica y está mejor integrada en los entornos de desarrollo. En un futuro trataremos sus diferencias y se hará un tutorial similar a este. Ambas herramientas son opensource y pueden ser descargadas gratuitamente en los siguientes enlaces:
- JUnit4: (Web Oficial | Descarga)
- Eclipse: ( Web Oficial | Descarga)
Integración en nuestro proyecto
Si optamos por integrar JUnit con un proyecto Maven, lo cual recomendamos, la forma de añadir la dependencia sería la siguiente:
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency>
Nota: En el POM incluimos la dependencia de JUnit con el scope test, esto hae que no se incluya la librería en los artefactos finales del proyecto.
Estructura de un test en JUnit
Lo primero que debemos hacer es definir la clase de pruebas. Aunque hay multitud de convenciones de nombre para los tests de JUnit, la solución más utilizada es agregar el sufijo Test al nombre de la clase probada. Para el nombre del método dentro de la clase de prueba se utiliza un texto identificativo de lo que debería realizar el método. Todos los métodos de prueba tienen que estar anotados con @Test
. Dentro del método de prueba se utilizarán distintos asserts. Las aserciones permiten verificar el comportamiento. En caso de incumplir dichas aserciones el método de prueba fallará identificando el error producido.
Un ejemplo podría ser el siguiente:
public class CalculadoraTest { @Test public void multiplicaPorCeroDebeRetornarCero() { // Probamos la clase Calculadora Calculadora calcTester = new Calculadora(); // asserts (condiciones que hacen pasar la prueba) assertEquals(0, calcTester.multiplica(10, 0), "10 x 0 debe ser 0"); assertEquals(0, calcTester.multiplica(0, 10), "0 x 10 debe ser 0"); assertEquals(0, calcTester.multiplica(0, 0), "0 x 0 debe ser 0"); } }
Ahora, cualquier desarrollador que implemente o modifique el método multiplica, al ejecutar las pruebas se asegura automáticamente que el nuevo código no rompe el comportamiento esperado.
Orden de ejecución de los tests
Aunque hay forma de indicarle a JUnit que se ejecuten los test por orden lexicográfico, por defecto se asume que los tests se ejecutan en orden arbitrario. Una batería de pruebas bien escrita no debe asumir ningún orden, los tests deben de ser totalmente independientes uno de los otros.
Anotaciones en JUnit
Como indicamos anteriormente, JUnit se basa en anotaciones para marcar los métodos como métodos de prueba o para configurar el conjunto de test. La siguiente tabla da una visión general de las anotaciones más relevantes.
Anotación | Descripción |
@Test | Indica a JUnit que se trata de un método de test. |
@Before | Se ejecuta siempre antes de cada método de test. Se suele utilizar para preparar el entorno de prueba (por ejemplo: leer datos de entrada, inicializar la clase, etc..) |
@After | Se ejecuta siempre después de cada método de test. Se suele utilizar para limpiar el entorno de desarrollo (por ejemplo: borrar datos temporales, restaurar valores por defecto, etc..) |
@BeforeClass | Se ejecuta solo una vez, antes de la ejecución de todos los tests. Se suele utilizar para ejecutar actividades de inicio muy costosas (por ejemplo: conexión a una base de datos). Estos métodos se tienen que definir como estáticos. |
@AfterClass | Se ejecuta solo una vez, después de la ejecución de todos los tests. Se suele utilizar para ejecutar actividades de cierre (por ejemplo: desconexión a una base de datos). Estos métodos se tienen que definir como estáticos. |
@Ignore | Indica a JUnit que el método de test está deshabilitado. Útil cuando el código ha cambiado sustancialmente y el test no está adaptado. Es de buena práctica indicar el motivo de por qué se ha deshabilitado. |
Aserciones en JUnit
Una vez creadas las condiciones es necesario validar si estamos obteniendo el resultado esperado o no. Para este fin JUnit dispone de una lista de funciones incluidas en su clase Assert. Los métodos assert comparan el valor obtenido con el valor esperado, lanzando una excepción si no son iguales. La siguiente tabla resume las más comunes, los parámetros entre corchetes son opcionales.
Método | Descripción |
assertTrue([mensaje], condición booleana) | Comprueba que la condición sea verdadera. |
assertFalse([mensaje], condición booleana) | Comprueba que la condición sea falsa. |
assertEquals([mensaje], valor esperado, valor actual) | Comprueba que dos valores sean iguales. Nota: en arrays comprueba su referencia, no el contenido!) |
assertSame([mensaje], valor esperado, valor actual) | Comprueba que ambos parámetros sean el mismo objeto. |
assertNotSame([mensaje], valor esperado, valor actual) | Comprueba que ambos parámetros no sean el mismo objeto. |
assertNull([mensaje], objeto) | Comprueba que el objeto sea nulo. |
assertNotNull([mensaje], objeto) | Comprueba que el objeto no sea nulo. |
fail([mensaje]) | Hace que el método falle. Debería ser utilizado solo para comprobar que una parte del código de test no se ejecute o para hacer fallar un test no implementado. |
Definiendo un test en JUnit
Ya tenemos todo lo necesario para empezar a escribir nuestros tests mediante el conjunto de anotaciones y métodos explicados anteriormente. Un ejemplo genérico podría ser el siguiente:
public class pruebaTest { @BeforeClass public static void setUpClass() throws Exception { // Tareas a realizar antes de ejecutar todos los tests } @AfterClass public static void tearDownClass() throws Exception { // Tareas a realizar después de ejecutar todos los tests } @Before public void setUp() { // Tareas a realizar antes de cada test } @After public void tearDown() { // Tareas a realizar después de cada test } @Test public void comprobarAccionYResultado() { // Creamos el entorno necesario para la prueba // Ejecutamos el método a probar // Usamos las aserciones para realizar la comprobación } public void funcionAuxiliar() { // Tareas auxiliares } }
Integración en Eclipse
Desde hace ya un tiempo Eclipse incorpora opciones para trabajar muy fácilmente con JUnit desde el IDE.
Si ya tenemos un proyecto, basta con seleccionarlo desde el explorador de proyectos de Eclipse y hacer clic derecho para obtener el menú. Elegimos la opción New -> Other -> JUnit test case. Basta elegir la versión de JUnit, elegir un nombre para la clase de pruebas (miClaseTest) y en la opción «Class under test» seleccionar la clase a testar (miClase). A continuación elegimos los métodos que deben ser testados.
Para ejecutar los test basta elegir la clase y hacer clic derecho para elegir la opción Run as -> JUnit test.
Para más información ver el tutorial de Chuidiang que explica este proceso con capturas de pantalla.
Conclusiones
Hemos realizado una introducción básica a las pruebas automatizadas dando una visión general de sus ventajas. Para la realización de las pruebas unitarias, básicas para los desarrolladores, se ha elegido la herramienta JUnit para Java debido a su facilidad de uso e integración en los principales IDEs. Se han definido las principales anotaciones y aserciones de JUnit así como indicaciones de dónde y cómo aplicar el testing unitario. En este tutorial básico se explica como obtener JUnit así como realizar un caso práctico en Eclipse. En un próximo tutorial se trataran aspectos más avanzados, como puede ser las reglas, los test parametrizados, la agrupación de test en suites o el testing de excepciones.
muy buena guía!, saludos.
Me alegro mucho de que te haya gustado. No dudes en comentar si tienes alguna duda o si deseas que amplíe el contenido. Saludos!