Cuanto tiempo son escribir!!! pero bueno, no esperéis mucho mas de un blog técnico 🙂

Hoy va de pruebas unitarias, para mi las primeras pruebas que han de hacerse, sin excusa, y que deberían ser programadas por otro miembro del equipo, no por uno mismo (salvo si uno es muy riguroso consigo mismo … ejem).

Para quien no esté familiarizado … ya estás tardando en mirar artículos, comprar libro, o que alguien te tutorice. Luego lee esta entrada 😉

En materia. Una de las cosas que no solemos hacer, y yo el primero, es probar que nuestro código «falla». Esa es la naturaleza de las pruebas, y es mas importante las pruebas que hagan que nuestro código «falle», que no que nos reafirme en nuestro buen trabajo, y que somos unos hachas. Cuando me refiero a que «falle», me refiero a que la prueba haga que lo que probamos no termine a través de un resultado, sino de una excepción. Muchos autores y expertos, recomiendan TDD, empezando por una premisa o precondición: nuestro código, métodos, clase o como queramos llamar, va a fallar (y no necesariamente por una mala praxis).

Escenario. Imaginemos que tenemos que importar datos de un excel. No entro en si hacéis TDD o probáis después de programar ¿qué es lo primero a probar? seguro que un alto porcentaje, ha pensado en verificar que los datos se importan correctamente … NOOOOOOO! lo primero, es qué pasa cuando los datos para realizar la importación son incorrectos. Si fuera una exportación, igual, y si acaso con mas importancia, o acaso vais a dejar que el usuario haga un trabajo para que al final se le diga que no puede exportar?

Una vez que tenemos el fallo o error, nos interesa saber si al programar lo hemos tenido en cuenta. Para ello tenemos que controlar que excepción se lanza. Sabremos si lo estamos considerando, si bien traducimos las excepciones a otras mas legibles, o bien las sustituimos por otras propias de la aplicación. Yo uso la combinación de ambas.

Entrando en materia, vamos a probar que al informar de una ruta incorrecta de un archivo excel, se está manejando la excepción. Normalmente lo hará la misma persona, la prueba y la programación de la importación, pero no debiera ser así. Que sean personas diferentes, no implica que un tester no tenga tantos o mas conocimientos de desarrollo que un programador. Como somos unos figuras, sabemos que si accedemos al excel por COM (por ejemplo), al dar una ruta incorrecta, nos va a devolver un COMException. Como persona que pruebo, y sabiendo que posiblemente la importación no dejará de ser una librería usada por terceros, quiero como mínimo una traducción a ArgumentException. La ruta viene en el constructor, lo he hecho así por comodidad, quien quiera estático e inicializar, es muy similar.

[TestMethod]
[TestCategory("Import & Export data")]
[Description("Lanza una excepción controlada, si la ruta del fichero no es correcta o hay problema en carga de fichero")]
[ExpectedException(typeof(ArgumentException))]
public void ExcelPathParameterThrowsHandledException()
{
   ExcelDataProvider excel = new ExcelDataProvider(String.Empty);
   var result = excel.ObtenerDatos();
}

Y aquí viene el objeto de esta entrada. En MSTest, similar a lo que pasa por ejemplo en JUnit, tenemos el atributo ExpectedException, para decir «oye, que si se me lanza esta excepción, la prueba ok». El problema es que espera EXACTAMENTE esa excepción, y como he comentado, no tiene el tester saber de la implementación interna de lo que prueba. Por ejemplo, puedo lanzar una excepción de este tipo, cuando tengo problemas al cargar el fichero.

public class ExcelPathArgumentException : ArgumentException
{
   public ExcelPathArgumentException() : base("Invalid file path, or excel file not found") { }
}

// Fragmento al inicializar el libro excel
try
{
   _libro = _app.Workbooks.Open(filePath);
}
catch (System.Runtime.InteropServices.COMException)
{
   throw new Exceptions.ExcelPathArgumentException();
}

Si la prueba os va a decir que ni hablar, que no pasa, porque no ha capturado un ArgumentException. Quizá haya quien no trabajé así, pero en mi opinión estamos trabajando con la calidad, y las pruebas muchas veces es nuestro contrato de mínimos. Aún así, el atributo no nos da mucha flexibilidad, y ataca al mantenimiento, porque como cambiemos las excepciones, a cambiar la prueba.

Para solucionar esto, vamos a emular el Assert.Throws de NUnit. Correcciones y otros frameworks, ya sabéis, en comentarios que enriquece. Qué hace NUnit, pues te da la posibilidad de realizar un Assert, pasando el tipo y el fragmento de código a través de un método. Traduciendo, vamos a hacer un método genérico, que admita un tipo de excepción, un delegado (Action) y no de error, si no se lanza la excepción esperada.

public static void AssertExceptionHandling<exception>(Action method)
            where exception : Exception
{
   try
   {
      method.Invoke();
   }
   catch (exception)
   { return; }
   catch (Exception ex)
   {
      Assert.Fail("Unexpected exception: " + ex.Message);
   }
   Assert.Fail("No exception thrown");
}

Doctores tiene la iglesia, pero esto es un buen comienzo, nuestro contrato de mínimos, que podemos ampliar. Límite, la imaginación.

Y ahora sí, ahora envolver todos los métodos que quiera, fragmentos a través de empresiones lambda, etc.

[TestMethod]
[TestCategory("Import & Export data")]
[Description("Lanza una excepción controlada, si la ruta del fichero no es correcta o hay problema en carga de fichero")]
public void ExcelPathParameterThrowsHandledException()
{
   TestUtils.AssertExceptionHandling<ArgumentException>(() =>
   {
      ExcelDataProvider excel = new ExcelDataProvider(String.Empty);
      var result = excel.ObtenerDatos();
   }
   );
}

Hay muchos frameworks que hacen estas cosas, pero bueno, así practicamos un poco. El tema de pruebas es un mundo, pero aún sigue siendo el hermano pobre, al que el presupuesto (en tiempo y/o dinero) siempre le da la espalda. Gran error. Clientes de empresas de software, exijan pruebas, e incluso formen parte de ellas, van a pagar un poco mas, pero los beneficios son tremendos (y ya les digo que el coste de mantenimiento menor).

Hasta otra.

PD: el código está generado para ejemplarizar la entrada.