Unit testing Buenas practicas

viernes, 23 de marzo de 2007

Los tests unitarios son una herramienta que ayuda el desarrollo de aplicaciones en escenarios de trabajo formados por equipos donde cada uno tiene un rol definido. En este artículo hablaremos sobre prácticas recomendables a la hora de utilizar Unit Testing.

Los tests de nuestros proyectos serán ejecutados por los desarrolladores y es a ellos en quien se debe pensar a la hora de crearlos, que sean amigables es una propiedad tan importante como que nuestros tests funcionen correctamente.

Verdades relativas de los tests unitarios

  • La detección de errores se ve facilitada.
  • El código es fácil de mantener.
  • El código es más comprensible.

Para que estas tres verdades se cumplan es necesario un buen desarrollo de tests unitarios. A continuación daremos una serie de recomendaciones útiles para lograrlo.

Testear lo correcto

Un test debe probar el que y no el como, debe asegurarse que la funcionalidad del código a probar se cumpla en forma correcta y no prestar atención a como lo logra.
Hacer test sobre la interfaz pública de las clases nos permitirá generalmente probar toda la funcionalidad disponible en la misma, mientras que si hacemos test sobre los métodos privados y/o protegidos seguramente nos estaríamos involucrando en la forma en que están implementadas las tareas y no si el fin se cumple correctamente. Cumpliendo con esta premisa sortearemos fallas en nuestros test cuando por algún motivo halla cambios en como se realizó determinada tarea.

Cuando un test falla en forma incorrecta

A veces encontramos un test que falla incluso sabiendo que los cambios realizados en el código son correctos. Esto generalmente nos llevará a la conclusión de que hemos hallado requerimientos incompatibles. En esta situación podremos tomar dos caminos diferentes...

  1. Borrar el test, luego de verificar que no será válido nunca más, ya que el requerimiento que lo hizo necesario ya no es un requerimiento del sistema.
  2. Modificar el test antiguo para adecuarlo a los nuevos requerimientos. Generalmente agregando un nuevo test que modifique las condiciones iniciales o finales del mismo.

Cobertura y ángulos de prueba

Como saber si el código se encuentra suficientemente cubierto por los test? Una prueba básica y muy efectiva es eliminar una línea de código de una funcionalidad completa, en el caso de que hecho esto un test falle esa porción de código tiene razón de ser, de lo contrario, será necesario agregar un nuevo test. En el caso de llegar a esta situación y no encontrar la forma de hacer un test que falle al comentar el código, es muy probable que el mismo no sea necesario.
Jamás sabremos cuando será necesario que otro programador modifique nuestro código, y si no se tienen los test necesarios para probar cada porción del mismo, no podrá notar si sus cambios perjudican otra funcionalidad que lo utilizaba parcial o totalmente
Seguramente en muchos casos un método necesitará varias pruebas, todas bajo diferentes condiciones para asegurar su correcto funcionamiento, uno de los errores más recurrentes en la escritura de un test es intentar abarcar todas las opciones dentro de un mismo test, esto es inadmisible ya que estaríamos violando la regla básica de este método de prueba, como su nombre lo indica estamos haciendo “Test Unitarios”, es decir, cada test prueba una y sólo una cosa. Nos daremos cuenta cuando estemos infringiendo esta regla si nos encontramos escribiendo una sentencia if, while, Etc... Respetar esta regla nos ahorrará, probablemente, tiempo valioso en prueba de nuestros test.

Los test deben ser sencillos de ejecutar

Facilitar la ejecución de test hará que los desarrolladores, para quienes esto es solo una perdida de tiempo en sus primeras aproximaciones, no se resistan a la idea de que ejecutar los test les ayuda a ganar tiempo y a encontrar errores en el código más fácilmente.
Existen básicamente dos tipos de test

  1. Los que pueden ejecutarse aleatoriamente.
  2. Los que necesitan una condición previa, para testear una situación en particular.

La idea es que todos nuestros test formen parte del primer grupo, de no ser posible, deberíamos agrupar unos separados de los otros con el objetivo de tener una serie de test que cualquiera sea capaz de ejecutar y probar que la funcionalidad básica esta cubierta y funcionando correctamente y otro grupo que pruebe más exhaustivamente que corramos menor cantidad de veces.
Los test del primer grupo se ejecutarían todo el tiempo, ante cualquier cambio por menor que sea, mientras que el segundo, después de haber completado la implementación de determinado requerimiento.

Desarrollo y mantenimiento

El test unitario debe ser escrito antes de programar una nueva funcionalidad o al modificar una que aún no tenía su test, no hay que confundirse en este paso el punto más importante en el comienzo de la generación de nuestros tests, “Los test deben fallar solo cuando la funcionalidad esté incompleta o no cumpla con los requerimientos”, comprender bien este punto nos ayudará a no hacer test que fallen por estar probando cosas ilógicas.
Un test creado no debe ser eliminado, sólo se llevará a cabo esta acción cuando los requerimientos sean modificados y la funcionalidad ya no sea requerida.

Eliminación de dependencias entre test

Anteriormente hablamos de dos grupos de test y las ventajas de tener test independientes que no necesiten un estado para salir airosos en su ejecución. Dos sentencias que ayudan bastante a esto son “TestInitialize” y “TestCleanup” que ayudan a mantener frescas todas las instancias de las clases utilizadas en nuestros test.
El método “TestInitialize” es el lugar donde debe encontrarse el código necesario para generar el estado básico para la ejecución de todos los test de una clase, si solo para uno de estos, parte del código no es relevante, no debería estar ahí. En estos casos es posible y recomendable, escribir un método de inicialización que se llame desde un test en particular, así estaríamos también modularizando el código, lo que nos facilitará la reutilización del mismo.

No incluir más de un ASSERT por test

Aparte de no estar reutilizando el código en nuestros test, esto nos hará perder más tiempo del necesario en resolver los problemas que detecten los mismos por los siguientes motivos. En primer lugar, cuando una sentencia assert falla se detiene la ejecución del método y por lo tanto las posteriores sentencias no serán ejecutadas y quizás fallen en el próximo intento. Necesitando así varias corridas de un mismo test para solucionar todos los problemas que era capaz de detectar. Eliminando la acumulación de estas sentencias en nuestros test no solamente nos libraremos de este problema, sino que, el tener de una vez la mayor cantidad de errores posibles nos ayudará a tener una idea más abarcativa del posible problema.

Crear test legibles

Hay un par de puntos básicos a seguir para lograrlo, dividir expresiones complejas en varias más sencillas y llamar a cada ítem con un nombre representativo a su funcionalidad dentro del código.
Aprender a nombrar cada ítem puede ser muy difícil pero sin duda es un punto más que importante a la hora de que el código sea legible y más aún a la hora de escribir test ya que con mirar el error que se detecta en uno de ellos deberíamos ser capaces de comprender que causó la falla y poder solucionarlo.
El nombre de un método de test se podría dividir en tres partes, el nombre del método bajo prueba, el estado o regla a probar y la salida esperada del mismo. No debería incluirse “Test” y/o el nombre de la clase bajo prueba ya que seguramente esto forma parte del nombre de la clase a la cual pertenecen nuestros test. Siguiendo estas reglas es probable que tengamos nombres mucho más largos que de costumbre pero no será relevante si esto ayuda a ganar tiempo cuando este apremia, a la hora de corregir bugs.

Conclusión

Como pudimos ver en este pequeño informe, escribir test no es una tarea trivial ni sencilla. Si bien escribir un test unitario no es más complicado de lo que es hacerlo con cualquier otro método y/o clase, para poder sacar provecho de la utilización de los mismos debemos tener en cuenta muchos aspectos que ayudarán a lograr mayor productividad utilizándolos. De lo contrario, no solo perderemos tiempo en escribirlos, sino también, en mantenerlos, correrlos, resolver los problemas por estos detectados y más.

Finalmente, la utilización de test puede ser de muchísima ayuda a la hora de producir y mantener una aplicación, solo debemos tener en cuenta que esta tarea no es sencilla y debemos prestar mayor atención en la redacción de los mismos.