El test, en cualquier de sus formas, es una de las más poderosas herramientas que tenemos los desarrolladores actuales para obtener un software de calidad, robusto y de más fácil mantenimiento. Incluso los test funcionales, que permiten realizar BDD, están entrando con fuerza en las técnicas generales que todo buen programador debiera conocer.
En el caso del ecosistema de desarrollo de Microsoft, Visual Studio 2010, este nos ofrece un potente framework de pruebas (Visual Studio Unit Testing Framework), que en este artículo voy a mostrar por medio de un sencillo ejemplo.
Creando el código
Este Hello World de testing no voy a iniciarlo con técnicas de desarrollo como pudiera ser TDD (Test Driven Development), aunque más adelante lo repasaré muy someramente. Así que lo primero es abrir un proyecto en C# cualquiera. En mi caso reutilizo un proyecto MVC3 + Razor, usado para este post sobre Linq.js.
A continuación añado un nuevo elemento del tipo clase y le dejo el nombre por defecto de class.cs. Y ahora voy a crear un método para ser testeado… algo complejo y retorcido:
public string holaMundo(string nombreUsuario)
{
nombreUsuario = "Hola, " + nombreUsuario;
return nombreUsuario;
}
Creando el test
Después de este enorme esfuerzo intelectual para obtener esta pieza de software voy a utilizar el Framework de Visual Studio para crear el test unitario de mi flamante nuevo método.
Para ello pulso con el botón derecho encima de la línea de declaración del método y, en el menú contextual escojo “Crear Test unitario”… y empieza la magia. El propio Visual Studio, al no encontrarlo, me ofrece crear un proyecto de testing. Si bien es cierto que puedo situar los test en el proyecto principal, yo aconsejo acostumbrarse a las particularidades de tener un proyecto externo al que estás probando. Así, si estoy desarrollando en una arquitectura de capas, desde este proyecto de test tendré acceso a todo lo probable de la solución entera.
Como ahora mismo me estoy centrando en hacer el test de este método, no te voy a contar para qué vale el porrón de código que ha creado el Framework, que para mí es lo peor del mismo. Pero créeme cuando te digo que cubre casi todos los casos que necesites. Por eso sobra tanto del mismo en esta práctica.
Pero lo que si voy a quitar es todo lo que nos sobra del método de test. Es decir, busco el método public void holaMundoTest() y elimino todo lo que hay por arriba a excepción del tag [TestMethod()]. A mí también me gusta quitar los comentarios de las líneas. Porque a partir de este momento los demás test los haré con copy&paste y modificándolos. Así el test quedaría así.
[TestMethod()]
public void holaMundoTest()
{
Class target = new Class();
string nombreUsuario = string.Empty;
string expected = string.Empty;
string actual;
actual = target.holaMundo(nombreUsuario);
Assert.AreEqual(expected, actual);
}
Fíjate en los dos cambios que he hecho sobre lo que debieras tener delante de tus ojos. He introducido un salto de línea antes y después de la ejecución del método a probar. Y he eliminado el último Assert.Inconcluse, ya que no me ofrece ningún valor a la prueba.
Probando el método con el test
Ahora sí que estamos listos a ver qué pasa. Pulso encima de la declaración del test con el botón derecho y le digo que lance la ejecución de las pruebas. El framework ejecuta la compilación de la aplicación y se abre la ventana de resultados de las pruebas; en la cual en unos instantes me saldrá un bonito “rojo” que indica que ha fallado el test.
Si te fijas en el mensaje y lees el código del test, el motivo del error está claro. En la declaración de los parámetros del test le estoy indicando que la cadena de texto esperada es un string.empty, pero mi método siempre devuelve un “Hola, ” por delante del nombre del usuario. Que en este caso también es una cadena vacía.
Modifiquemos entonces el test, primero para indicarle un nombre de usuario que le voy a pasar al método para probarlo: “Juan Carlos” y después en expected le diré que lo que estoy esperando que nos devuelva y que compararé en el Assert es “Hola, Juan Carlos”.
[TestMethod()]
public void holaMundoTest()
{
Class target = new Class();
string nombreUsuario = “Juan Carlos”;
string expected = “Hola, Juan Carlos”;
string actual;
actual = target.holaMundo(nombreUsuario);
Assert.AreEqual(expected, actual);
}
Ahora sí, al lanzar nuestro test obtendremos un “verde”. La prueba de que mi método pasa el test definido.
Un poquito de TDD
Test Driven Development es una técnica de programación compuesta por dos partes diferenciadas. La primera es Escribir las pruebas primero (Test First Development)y después la Refactorización del código (Refactoring). Sobre estos conceptos se pueden y se han escrito libros, pero yo voy a aprovechar nuestro ejercicio para darle un primer vistazo a su potencia, haciendo más complejo el método de HolaMundo de una forma segura.
Como veo, si al método de saludar le remito un nombre, me devuelve un saludo a ese nombre. Pero, ¿si quiero que cuando se le envíe una cadena de texto vacía nos devuelva “Hola, visitante”? Pues aquí entramos en TDD. Bueno realmente en su primera parte, porque refactorizar, lo que es refactorizar, nos llevaría a un ámbito mucho más lejano del objetivo de este tutorial hablando de LOC, Inyección de Dependencias y SOLID.
Así pues hago un test que sé que va a fallar, utilizando el antipatron de Copy&Paste y poniéndole un nombre descriptivo al nuevo test.
[TestMethod()]
public void holaMundoCuandoUsuarioEsStringEmpty()
{
Class target = new Class();
string nombreUsuario = string.Empty;
string expected = “Hola, Visitante”;
string actual;
actual = target.holaMundo(nombreUsuario);
Assert.AreEqual(expected, actual);
}
Bueno ahora me voy al método y modifico solo y exclusivamente lo que sea necesario para que pase el test. Es importante esto. De hecho el inicio de todo TDD debe empezar con un método que retorne directamente lo esperado, para después refactorizando obtener toda la arquitectura que nos permita cumplir los objetivos.
public string holaMundo(string nombreUsuario)
{
if (string.IsNullOrEmpty(nombreUsuario))
{ nombreUsuario = "Visitante"; }
nombreUsuario = "Hola, " + nombreUsuario;
return nombreUsuario;
}
Si lanzo los test, todos, ahora debería tener todos en verde… y así he logrado nuestro primer desarrollo en TDD.
Pero espera que nos falta eso de refactorizar... como este código es tan sencillo lo único que podríamos hacer es separar la responsabilidad del que recibe el usuario y de quien lo muestra; pero eso va mucho más allá del contenido de este artículo. Pero si podría hacer una nueva prueba que me obligue a refactorizar el método, como pudiera ser el evitar que el nombre sea demasiado largo. Y que si lo fuera que nos diera un mensaje avisándolo.
El test, por ejemplo, podría ser.
[TestMethod()]
public void holaMundoCuandoUsuarioEsMayorDe20Caracteres()
{
Class target = new Class();
string nombreUsuario = “Esto es un nombre de usuario de más de 20 caracteres”;
string expected = “Lo siento, tu nombre es demasiado largo para entenderlo”;
string actual;
actual = target.holaMundo(nombreUsuario);
Assert.AreEqual(expected, actual);
}
El cual si lo lanzo me devuelve un test fallido y me obliga a cambiar mi método por:
public string holaMundo(string nombreUsuario)
{
if (string.IsNullOrEmpty(nombreUsuario))
{ nombreUsuario = “Visitante”; }
if (nombreUsuario.Count() < 20)
{ nombreUsuario = “Hola, “ + nombreUsuario; }
else
{ nombreUsuario = “Lo siento, tu nombre de usuario es demasiado largo para entenderlo”; }
return nombreUsuario;
}
Lo lanzo y… sorpresa!! Un test fallido.
La robustez en el código gracias a los test
¿Pero qué ha pasado? En la ventana de resultado el mensaje de la causa del fallo no se termina de ver, por lo cual si hago un doble click en el test me encuentro con una nueva pestaña en donde tengo mucha información sobre el test y el porqué de su fallo.
Y aquí vemos la potencia de hacer test unitarios para obtener la robustez de nuestras aplicaciones. La causa es que la cadena de texto de que nos devuelve el método es diferente a la que estoy esperando en el test. Pero imagínate si estuvieramos hablando de miles o millones de líneas y que esté método fuera utilizado en cientos de sitios por miles de clases y que se me ocurre cambiar el tipo de datos del parámetro del método, como pudiera ser que en vez de recibir el nombre de usuario como un string, lo recibiera con un stringBuilder.
Los test alertarían de esos errores de regresión provenientes de tocar un código que afecta a otro que fue construido por otra persona en tiempos pasados y con otro objetivo. Y esto implica que puedo ponerme a trastear, modificar o ampliar cualquier código con la seguridad de que si rompo algo, esto será visible de forma inmediata al pasarle la batería de test.
Solamente por estas dos razones, seguridad en el desarrollo y robustez en su mantenimiento, soy de la absoluta opinión de que los test no son opcionales. Todo aquel que se quiera llamar buen programador debería estar habituado a utilizarlos.
Más información | Tutorial: Crear y ejecutar pruebas unitarias. MSDN
Lectura Recomendada | Dirigido por Test por Carlos Ble