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.