Herramientas para el Desarrollo de Pruebas Unitarias

Frameworks para el desarrollo de pruebas unitarias

En Java existen básicamente dos frameworks con una adopción importante en el mercado JUnit y TestNG. TestNG tiene ciertas capacidades que no están incluidas en la distribución básica de JUnit; muchas de estas capacidades, sin embargo, pueden ser incluidas empleando algunas de las extensiones para JUnit.

JUnit

JUnit 4.x es probablemente el framework con más adopción y soporte para el desarrollo de pruebas unitarias en Java.  En esencia, permite la definición de los diferentes componentes de una prueba: preparación, ejecución, verificación y limpieza. JUnit provee sus servicios mediante una librería de anotaciones y clases además de un muy buen soporte en los entornos integrados de desarrollo como Eclipse, NetBeans o IntelliJ IDEA.

Definición de los métodos de prueba

@Test. Anotación a nivel de método que configura/define un método como una prueba de software. Los métodos marcados con esta anotación en una clase definen un conjunto/suite de pruebas orientadas a probar una responsabilidad.

Definición del fixture de la prueba

En las primeras etapas del desarrollo de una suite de pruebas, el fixture se prepara en el mismo método de prueba. Sin embargo, si un mismo fixture puede ser empleado para ejecutar varias pruebas, JUnit provee algunas anotaciones que ayudan a reducir esta repetición:

@Before. Anotación a nivel de método empleada para solicitar la ejecución de un método antes de la ejecución de cada uno de los métodos de prueba.

@BeforeClass. Anotación a nivel de método empleada para solicitar la ejecución de un método antes de la ejecución de algún método de prueba. Inicializar la prueba en el constructor de la clase tendría un efecto similar.

Verificación

La verificación se realiza empleado alguno de los métodos estáticos de la clase org.junit.Assert:

  • assertEquals()Emplea el métodos equals() para verificar si dos objetos son iguales.
  • assertTrue()Verifica si una condición es true.
  • assertFalse()Verifica si una condición es false.
  • assertNull()Verifica si un objeto null.
  • assertNotNull()Verifica si un objeto no es null.
  • assertNotEquals()Emplea el métodos equals() para verificar si dos objetos no son iguales.
  • assertArrayEquals()Verifica si dos arreglos tienen los mismos objetos en las mismas posiciones.
  • assertSame()Emplea el operador == para verificar si dos objetos son iguales.
  • assertNotSame()Emplea el operador == para verificar si dos objetos no son iguales.
  • assertThat()Empleado para verificar condiciones empleando Matchers como los provistos por librerías como hamcrest o assertJ para mejorar la legibilidad de las verificaciones.

Limpieza

@After. Anotación a nivel de método empleada para solicitar la ejecución de un método después de la ejecución de cada uno de los métodos de prueba.

@AfterClass. Anotación a nivel de método empleada para solicitar la ejecución de un método después de la ejecución de todos los métodos de prueba.

Dobles de Prueba

En la mayoría de las ocasiones, la implementación de una responsabilidad se apoya en la funcionalidad provista por otras clases (colaboradores).  En las pruebas unitarias (aquellas centradas en una clase) es necesario reemplazar los colaboradores por dobles de prueba con el objetivo de proveer un entorno controlado en el cual podamos ejecutar la prueba unitaria centrada en la funcionalidad bajo prueba.

Para facilitar el reemplazo de las implementaciones de los colaboradores empleadas en producción por dobles de prueba es fundamental que el diseño de la clase bajo prueba siga el Principio de Inversión de Dependencias eliminando las dependencias a nivel de código fuente entre la clase bajo prueba y la implementación de los colaboradores.

Tipos de dobles de prueba

Dependiendo del propósito de la prueba unitaria, esta puede requerir de los siguientes tipos de dobles de prueba:

Dummy. Es una implementación donde el cuerpo de los métodos están vacíos (un dummy object no hace nada). Se emplean cuando sólo se requiere un objeto de este tipo en el contexto de prueba pero no hay ninguna interacción con dicho objeto durante la ejecución de la prueba.

Stub. Un stub es un dummy (el cuerpo de los métodos están vacíos) pero al menos alguno de sus métodos devuelven valores esperados por la prueba (durante la prueba se interactúa con el doble de prueba para obtener algún valor). Se emplean cuando se necesita ejercitar una ruta de ejecución asociada a una estructura condicional que depende de los valores devueltos por un colaborador.

Spy. Un spy es un stub (algunos de sus métodos devuelven valores esperados por la prueba para dirigir el flujo de ejecución a una sección de interés para la prueba) pero también recuerdan detalles relacionados a las llamadas a sus métodos que luego pueden ser reportados a la prueba para su verificación (si el método fue llamado, cuantas veces se llamó, con qué argumentos, etc.).

Verificación del Estado vs Comportamiento

Los enfoques más empleados para probar una funcionalidad son:

La verificación del estado. Con este enfoque el desarrollador de la prueba provee un conjunto de ejemplos de datos de entrada y verifica los resultados esperados. La mayor ventaja de este enfoque es el poco acoplamiento entre la prueba y la implementación de la funcionalidad a probar (si cambia la implementación, las pruebas no tienen que cambiar); la mayor desventaja tiene que ver con asegurar que el conjunto de ejemplos sea suficientemente exhaustivo  y no se hayan omitido casos que podrían ocurrir en producción y causar un resultado inesperado.

La verificación del comportamiento. Con este enfoque el desarrollador verifica que la ejecución de la funcionalidad haya realizado ciertas operaciones que se esperan en una implementación válida. Por ejemplo, si estamos probando un algoritmo para calcular el producto de 2 x 3 aplicando sumas sucesivas, se verifica que la operación suma se haya ejecutado 3 veces con el valor 2. La mayor ventaja de este enfoque es que se considera brinda mayor certeza sobre el comportamiento esperado; la mayor desventaja radica en que la prueba tiende a estar más acoplada a la implementación de la funcionalidad.

Finalmente, además de estos enfoques, existen estrategias y herramientas alternativas que vale la pena explorar con mayor profundidad como las pruebas basadas en propiedades con generadores de ejemplos.

Creación de los dobles de prueba

La creación de los dobles de prueba es parte integral de desarrollo de la prueba. En la mayoría de las ocasiones la creación de los dobles de prueba, debido a la simplicidad de su funcionalidad, puede realizarse sin la asistencia de librerías o frameworks. Sin embargo, en ciertas ocasiones podría ser necesario recurrir a herramientas de este tipo como Mockito,  PowerMock o EasyMock.

Referencias

[1] Practical Unit Testing with JUnit and Mockito

[2] xUnit Test Patterns: Refactoring Test Code

[3] xUnit Test Patterns: Catálogo

[4] Mocking with Mockito and Powermock

[5] Mocking series @ baeldung

Leave a Reply