DNM+ Online
dotnetmania 2.0
Testeo unitario con NUnit
Sin duda el testeo unitario es una de las técnicas relacionadas con la calidad del software que mayores beneficios aporta al proceso de desarrollo. Aunque es una técnica clásica, en los últimos tiempos se ha popularizado de la mano de las metodologías ágiles de desarrollo de software y gracias a la aparición de herramientas que facilitan la escritura, ejecución e integración de los test en el proceso de desarrollo.

Testeo unitario

¿Qué es el testeo unitario? A mi me gusta definir el testeo unitario como escribir código que pone a prueba otro código de manera que podamos realizar pruebas de forma automatizada. Un test de unidad es código que comprueba que el comportamiento de otro código es acorde a las especificaciones. Los test de unidad deben centrarse en probar que el código se ejecuta sin errores inesperados, que el código es seguro, que el rendimiento del código es correcto y que el código se comporta de acuerdo a las especificaciones del software que estamos construyendo. Las ventajas de utilizar testeo unitario en nuestro proceso de desarrollo son muchas, de hecho son tantas que si tuviese que elegir una única técnica relacionada con la calidad del software que llevar a cabo en mis proyectos, ésta sería el testeo unitario. Entre las múltiples ventajas cabe citar: •    Escribir test de unidad obliga a tener en cuenta la calidad: La calidad de software es lo primero que tiramos por la ventana los desarrolladores cuando estamos apurados de tiempo (que es siempre), abandonar la preocupación por la calidad parece una ganancia de tiempo, pero sólo lo es corto plazo. Si estamos obligados a escribir test de unidad, nunca nos olvidaremos de la calidad. •    Escribir test de unidad ayuda a mantener la calidad: Es mucho menos costoso mantener la calidad del software, que una vez que ésta se ha degradado, volver a parámetros aceptables de calidad. Los tests unitarios ayudan a mantener la calidad por que permiten en cualquier momento comprobar de manera rápida y fácil que el código sigue comportándose de acuerdo a la especificaciones, y que el nuevo código añadido al sistema no ha estropeado el comportamiento del antiguo. •    El testeo unitario ayuda a soportar otras buenas prácticas de desarrollo, como las construcciones diarias y los tests de humo: Construyendo a diario nuestro sistema y realizando "test de humo" (probar lo esencial del software una vez construido cada día) aseguramos que los problemas de integración se corrigen pronto. Si antes de realizar el test de humo, pasamos los tests unitarios, aseguramos que podremos hacer unas mínimas pruebas del sistema. La integración continua es mucho más sencilla si tenemos tests unitarios. •    El testo unitario permite mejorar nuestra base de código sin temor a que éste deje de funcionar: A menudo los desarrolladores evitamos mejorar código para hacerlo más eficiente o más legible por el temor a introducir errores ("si funciona no lo toques"), si tenemos tests de unidad para el código que pretendemos mejorar, esto asegurara que su comportamiento seguirá siendo el correcto. •    Los tests de unidad detectan tempranamente problemas de rendimiento: Es habitual que en ocasiones nuevo código degrade de manera significativa el rendimiento de nuestro software por errores de programación o incluso de arquitectura. Una batería de test que ayer tardaba en ejecutarse dos minutos y hoy tarda quince minutos puede ponernos en la pista de que algo pasa con el código añadido al sistema. •    Los tests de unidad documentan el sistema: A menudo es mucho más fácil deducir cuáles son las responsabilidades de una clase observando sus test de unidad que leyendo su código fuente. •    Los tests de unidad facilitan la portabilidad del software: Una vez portado el software a la nueva plataforma, es sencillo comprobar que el comportamiento es el correcto ejecutando las baterías de test. •    Los tests de unidad incrementan la modularidad y evitan el acoplamiento: Los tests prueban unidades (clases, librerías, componentes, etc.) y para que sea sencillo escribir test y ejecutarles, las clases deben ser lo más autónomas posible, de manera que no sea necesario montar una compleja infraestructura para correr el test de una determinada unidad. •    Los tests de unidad ayudan a mejorar el soporte al usuario: A menudo es una buena táctica enviar al usuario una batería de test para diagnosticar un problema; es fácil utilizar los tests de unidad como una herramienta de diagnóstico. Esta técnica funciona especialmente bien si se tiene la disciplina de escribir un test de unidad que asegure la corrección de errores anteriormente reportados por los usuarios. Es condición necesaria que los tests de unidad, una vez terminada su ejecución, dejen el sistema en el estado en el que se encontraba. Como véis, existen motivos más que suficientes para escribir test de unidad, aún así, el principal inconveniente con el que me he encontrado a la hora de utilizar testeo unitario en los proyectos en los que he participado, es la reticencia por parte de los miembros del equipo de desarrollo. En principio, el uso de test de unidad se ve como "escribir el doble de código", el hecho es que sí suponen escribir algo más de código, pero nunca el doble. Estimo que el tamaño del código de testeo rara vez es más del 20% del tamaño del código a probar, y ya hemos visto las importantes ventajas de ese código adicional. También tengo que decir que una vez vencida esa reticencia inicial, en algunos casos muy fuerte, nunca nadie ha propuesto abandonar la escritura de test de unidad. Por lo que concluyo que las reticencias iniciales siempre se deben a la ignorancia de las ventajas que esta técnica aporta. Testeo unitario en .NET con NUnit Ya he comentado que uno de los factores que han popularizado la adopción de los tests de unidad es la aparición de frameworks y herramientas que permiten escribirlos y ejecutarlos. En el caso de la plataforma .NET, la herramienta por excelencia es NUnit. Esta herramienta, utilizando características avanzadas de .NET como atributos y reflexión, ha convertido el escribir y ejecutar test de unidad en un tarea simple. Podemos descargar NUnit desde www.nunit.org. Su instalación es muy simple. Una vez instalado NUnit tenemos a nuestra disposición una serie de librerías que nos permitirán escribir test unitarios de una manera simple y elegante. Escribir un test unitario Vamos a crear una simple clase que representa una cuenta bancaria. No es un ejemplo muy original ya lo sé, pero sí que es uno útil y simple. Esta clase presenta los siguientes miembros: •    Una propiedad Saldo que devuelve el saldo de la cuenta. •    Una propiedad Id que devuelve el identificador de la cuenta. Se establece en el constructor. •    Un método RealizarAbono que añade una cantidad al saldo de la cuenta. •    Un método RealizarCargo que quita una cantidad al saldo de la cuenta. •    Un método RealizarTransferencia que transfiere una cantidad a otra cuenta. Este método lanzará una excepción cuando intentemos realizar una transferencia que tenga como destino la propia cuenta. El código de esta clase es el del fuente 1. Conocidos estos datos, ya podríamos saber qué puntos debería comprobar un test de unidad que comprobase el correcto funcionamiento de esta clase: •    Que RealizarAbono incrementa el saldo en el valor correcto. •    Que RealizarCargo decrementa el saldo en valor correcto. •    Que RealizarTransferencia disminuye el saldo de la cuenta origen y aumenta el de la cuenta destino y que el balance permanece correcto. •    Que RealizarTransferencia lanza una excepción si la cuenta de origen es la misma que la destino. Empezaremos creando una nueva librería de clases que contendrá nuestros tests de unidad y añadiendo una referencia a la librería de test de NUnit, NUnit.Framework lo que pondrá a nuestra disposición los atributos que utiliza NUnit para distinguir los conjuntos de test y cada test unitario, así como las clases que nos permiten comprobar el adecuado funcionamiento de nuestro código. Las clases y atributos que utilizaremos para escribir test de unidad se encuentran en el espacio de nombres NUnit.Framework. El atributo TestFixtureAttribute nos permite marcar una clase como contenedor de un conjunto de test de unidad. Cada uno de los tests de unidad se implementa como un método de esa clase y se identifica mediante el atributo TestAttribute. Estos atributos permiten a NUnit detectar los tests y ejecutarlos. NUnit marcará como test fallido todo test del que reciba una excepción, siempre y cuando el propio test no la controle. Si queremos comprobar que bajo ciertas condiciones un método lanza una determinada excepción (por ejemplo el método RealizarTransferencia cuando se especifica la propia cuenta como destino) tenemos que utilizar el atributo ExpectedExceptionAttribute, especificando como parámetro del mismo el tipo de la excepción esperada. Los tests de unidad no sólo están orientados a que no se produzcan excepciones o que éstas sean controladas o esperadas, además deben asegurar el buen funcionamiento de nuestra clase en el sentido de que realice el trabajo para el que está diseñada (por ejemplo, que el método RealizarCargo disminuya el saldo de la cuenta en la cantidad especificada). La clase Assert de NUnit proporciona una serie de métodos que permiten escribir comprobaciones de ese tipo, ver la tabla 1, "Métodos de la clase Assert". Existen además otros atributos que nos permiten agrupar conceptualmente tests o incluso especificar código que se ejecute antes (por ejemplo, para preparar el entorno para la realización de los tests) o después de un grupo de tests (típicamente utilizado para devolver el entorno a su estado inicial), ver la tabla 2, "Atributos de NUnit". Cómo organizar nuestros tests Voy a comentar aquí cómo acostumbro a organizar los tests unitarios de los proyectos en los que trabajo. Supongo que existirán otras formas de hacerlo y que algunas incluso sean mejores, no lo sé, pero lo que si sé es que ésta me es cómoda. La técnica que utilizo consiste en generar por cada assembly, un assembly paralelo que contiene los tests unitarios. Es decir, si tengo un assembly llamado, MyAssembly genero otro llamado MyAssembly. Test y ambos los añado a la misma solución. Para cada una de las clases contenidas en MyAssembly, llamémosla MyClass, y supongamos que está contenida en el namespace MyAssembly.MyNamespace, generó una clase paralela en MyAssembly.Test, con nombre MyClassTest y ubicada en el namespace MyAsembly.MyNamepace.Test. Siguiendo estas pequeñas reglas me es sumamente fácil encontrar los tests de una clase y clase a la que corresponde un determinado test. A la hora de nombrar las funciones que implementa un test de una función, simplemente nombro la función que realiza el test como la función a testear, seguida del subfijo test; así para la función MyFunction el test se llamará  MyFunctionTest. En bastantes ocasiones, una única función no es suficiente para probar a fondo una función de una clase; en esos casos añado un número correlativo a la función de prueba, MyFucntionTest1, MyFucntionTest2,... A veces es conveniente asociar tests con bugs, para asegurar que estos no se vuelven a introducir en el código. Si tu sistema de bug tracking es capaz de hacer los bugs accesibles mediante una URL, es una buena práctica poner ésta en la sección <remarks> de la documentación XML del método de prueba, si no, añadir un sufijo al nombre de la función que identifique el bug, MyFunctionTestBug123. También puede ser útil utilizar ambas técnicas a la vez. Por último, recordar que las clases que implementan los tests siguen siendo clases, y que por tanto, permiten usar todas las técnicas de la programación orientada a objetos, como la herencia para reutilizar funcionalidad de un test en otro. Cómo correr y depurar nuestros tests Una vez construidos nuestros tests tenemos que poder correrlos. Para ello NUnit provee dos herramientas que permiten cargar y correr tests, Nunit-console, que como su nombre indica es una aplicación en consola y Nunit-gui que es una aplicación de ventana. Nunit-console es muy útil para automatizar e integrar la ejecución de los tests en el proceso de construcción de nuestro proyecto. Aquí me centraré en Nunit-gui. Para correr un test unitario con Nunit-gui simplemente tenemos que cargarlo utilizando la opción "Open" del menú "File", y seleccionando el assembly que contiene los tests. Hemos de asegurarnos de que este assembly tiene accesible el assembly que contiene la clases a probar, lo que básicamente implica que ambos estén en el mismo directorio. Una vez hecho esto, un árbol muestra los tests que tenemos disponibles, y podemos utilizar [F5] para iniciarlos. Una vez los tests han terminado, NUnit presentará los resultados de los mismos mediante un código de colores. Los tests que hayan fallado, aparecerán en rojo y podremos obtener más información sobre el fallo en el panel de la derecha haciendo clic sobre los mismos en el árbol. Podemos utilizar NUnit para depurar los tests y las clases que los tests prueban. Para ello simplemente deberemos, antes de iniciar los tests, y con ellos cargados, asociar el depurar al proceso Nunit-gui.exe, teniendo la solución que contiene las clases y sus tests cargada. Una vez hecho esto, cualquier punto de ruptura que pongamos en las clases o los tests será reconocido por el depurador. El futuro, testeo unitario en Visual Studio 2005 Parece que Microsoft también a descubierto la importancia del testeo unitario y, a partir de la nueva versión de Visual Studio, el testeo unitario va a estar totalmente integrado dentro de esta herramienta de desarrollo. Contaremos con nuevos tipos de proyecto y plantillas de clase orientadas a escribir test y además podremos correr estos tests sin salir del entorno. Esta posibilidad en Visual Studio 2003 existe gracias a addins como TestDriven.Net (http://sourceforge.net/projects/nunitaddin). Además parece ser que se van a poder importar los tests escritos para NUnit, y que estos serán convertidos de manera automática a test de Visual Studio. Entre los diferentes tipos de tests con los que contaremos en Visual Studio 2005 se encentran los tests unitarios, tests de carga o tests de Web, además de plantillas para describir tests a realizar manualmente.

blog comments powered by Disqus
autor
referencias