Gerardo Contijoch

Experiencias del día a día trabajando con .NET – ASP.NET, C#, ASP.NET MVC y demas…

Posts Tagged ‘Testing’

Acceder directamente al test que falló en MSTest

Posted by Gerardo Contijoch en diciembre 16, 2008

Una de las cosas que más me molesta de ejecutar los tests dentro de Visual Studio (MSTest), es que para llegar al código de un test que falló, tengo que acceder a la ventana de resultados del test, y recién ahí hacer click en el link al archivo (que nos lleva directamente a la línea que falló).

Afortundamente, Simone Chiaretta encontró la solución al problema, ¡y la encontró dentro del propio Visual Studio!

Les dejo el link.

¡Nos vemos en el próximo post!

Publicado originalmente en https://gerardocontijoch.wordpress.com.

Posted in Testing, Visual Studio | Etiquetado: , , , | Leave a Comment »

Uso de ExpectedExceptionAttribute para testear excepciones

Posted by Gerardo Contijoch en diciembre 2, 2008

Hace tiempo que se viene hablando sobre el uso (¿mal uso?) del atributo ExpectedExceptionAttribute en NUnit o MSTest (y en cualquier otro framework de testing que lo tenga). En realidad el problema esta en el hecho de que se use el atributo y no en la forma de usarse. Según la documentación de NUnit, ExpectedExceptionAttribute “es la manera de indicar que la ejecución de un test va a provocar una excepción”.

Veamos un ejemplo de su uso (usando MSTest). Supongamos que tenemos que modelar un negocio en donde se venden cosas. Este es el código a testear:

    7
    8 public class Negocio {
    9
   10     public int Stock { get; private set; }
   11
   12     public Negocio(int stockInicial) {
   13         if (stockInicial <= 0) {
   14             throw new ArgumentOutOfRangeException("El stock no puede ser menor o igual a 0.");
   15         }
   16
   17         this.Stock = stockInicial;
   18     }
   19
   20     public void Vender(int cantidadDeUnidades) {
   21
   22         if (Stock < cantidadDeUnidades) {
   23             throw new ArgumentOutOfRangeException("No hay suficiente stock para vender.");
   24         }
   25
   26         /* procesamiento de la venta... */
   27
   28         this.Stock -= cantidadDeUnidades;
   29     }
   30 }

Como se ve es muy sencillo e incluye un par de validaciones básicas.

Ahora creamos un test para el método Vender para asegurarnos de que se esta actualizando correctamente el stock luego de realizar una venta.

   36
   37 [TestMethod]
   38 public void Vender_actualiza_stock_luego_de_venta() {
   39
   40     // Inicializamos nuestro negocio con un stock de 10 unidades
   41     var n = new Negocio(10);
   42
   43     // Vendemos 5
   44     n.Vender(5);
   45
   46     Assert.AreEqual(5, n.Stock, "No se actualizó correctamente el stock de unidades luego de una venta.");
   47 }

Si ejecutamos el test, veremos que es pasado sin problemas. Ahora creamos un test para asegurarnos que falla cuando no hay stock suficiente.

   48
   49 [TestMethod]
   50 [ExpectedException(typeof(ArgumentOutOfRangeException))]
   51 public void Vender_falla_si_no_hay_suficiente_stock() {
   52
   53     // Inicializamos nuestro negocio
   54     var n = new Negocio(0);
   55
   56     n.Vender(5);
   57
   58 }

Noten el uso de ExpectedExceptionAttribute para asegurarnos que el método Vender efectivamente falla provocando una excepción del tipo ArgumentOutOfRangeException.

Si ejecutamos el test, el mismo va a ser pasado sin mayores inconvenientes (debido a que va a fallar como es esperado), pero hay un grave problema oculto (que en este ejemplo es bastante evidente, pero no siempre lo es): El test esta fallando, pero no donde nosotros esperamos que falle (en el método Vender), sino que falla en el constructor de la clase Negocio (debido a que la cantidad pasada como stock inicial es inválida). Esto significa que nunca llegamos a ejecutar el método Vender y por lo tanto, no sabemos si efectivamente el mismo falla o no cuando no hay stock suficiente.

Para solucionar este problema, lo que se puede hacer es encerrar el código que debe fallar entre bloques try/catch y verificar que efectivamente se esta lanzando una excepción en ese punto (y no antes o en otro lugar), pero esa no es una solución muy elegante y hay que escribir demasiado código extra. Es por ello que las últimas versiones de los frameworks de testing mas populares incluyen clases y métodos helpers que nos facilitan la tarea de verificar que se esta lanzando una excepción.

Por ejemplo, NUnit 2.5 (aún en etapa beta) tiene el método Throws (de la clase Assert) que nos facilita la tarea haciendo que el código para testear la excepción quede de la siguiente manera:

   56 Assert.Throws<ArgumentOutOfRangeException>( () => n.Vender(5) );

Si la llamada al método Vender NO lanza una excepción del tipo ArgumentOutOfRangeException, entonces se lanza una excepción del tipo AssertionException, o lo que es lo mismo, se aborta el test en curso.

Lamentablemente MSTest no incluye ningún método similar y es por ello que tome la idea e hice un ‘port’ de ese método a MSTest:

    5
    6 static class CustomAssert {
    7
    8     /// <summary>
    9     /// Verifica que la ejecución de <paramref name="accion"/> lance una excepción.
   10     /// </summary>
   11     /// <typeparam name="TExcepcion">Tipo de la excepción esperada.</typeparam>
   12     /// <param name="accion"><see cref="Action"/> a ejecutar.</param>
   13     /// <returns>Excepción lanzada.</returns>
   14     /// <exception cref="AssertFailedException">La ejecución de <paramref name="accion"/> no produjo una excepción del tipo esperado.</exception>
   15     public static TExcepcion Throws<TExcepcion>(Action accion)
   16         where TExcepcion : System.Exception {
   17
   18         return Throws<TExcepcion>(accion, null);
   19     }
   20
   21     /// <summary>
   22     /// Verifica que la ejecución de <paramref name="accion"/> lance una excepción.
   23     /// </summary>
   24     /// <typeparam name="TExcepcion">Tipo de la excepción esperada.</typeparam>
   25     /// <param name="accion"><see cref="Action"/> a ejecutar.</param>
   26     /// <param name="mensaje">Mensaje a mostrar en caso de que no se lance la excepción esperada.</param>
   27     /// <returns>Excepción lanzada.</returns>
   28     /// <exception cref="AssertFailedException">La ejecucion de <paramref name="accion"/> no produjo una excepción del tipo esperado.</exception>
   29     public static TExcepcion Throws<TExcepcion>(Action accion, string mensaje)
   30         where TExcepcion : System.Exception {
   31         try {
   32             accion.Invoke();
   33         } catch (Exception ex) {
   34             CompararTiposYFallarSiSonDistintos(typeof(TExcepcion), ex.GetType(), mensaje);
   35             return ex as TExcepcion;
   36         }
   37
   38         Assert.Fail(string.Format("Se esperaba una excepción del tipo {0}, pero no se produjo ninguna.", typeof(TExcepcion).ToString()));
   39
   40         // Llamada necesaria para que compile
   41         return null;
   42     }
   43
   44     private static void CompararTiposYFallarSiSonDistintos(Type tipoEsperado, Type tipoActual, string mensaje) {
   45         if (tipoEsperado != tipoActual) {
   46             if (string.IsNullOrEmpty(mensaje)) {
   47                 mensaje = string.Format("Se esperaba una excepción del tipo {0}, pero se produjo una excepción del tipo {1}.", tipoEsperado.ToString(), tipoActual.GetType().ToString());
   48             }
   49
   50             Assert.Fail(mensaje);
   51         }
   52     }
   53
   54 }

Tuve que crear la clase CustomAsserts debido a que la clase Assert es estática y por lo tanto no se le pueden agregar métodos de extensión. Un detalle a tener en cuenta es que este método siempre devuelve la excepción producida por lo que se puede verificar que los valores de las propiedades de la misma sean los correctos. Esto ultimo es muy útil con excepciones del tipo ArgumentException (y derivadas) ya que normalmente querremos verificar que los argumentos que tiene problemas sean los que esperamos y no otros. Veamos un ejemplo:

   17
   18 public void MetodoConParametros(string param1, int param2) {
   19     if (param1 == string.Empty) {
   20         throw new ArgumentException("param1 no puede ser vacío.", "param1");
   21     }
   22
   23     if (param2 > 5) {
   24         throw new ArgumentException("param2 no puede ser mayor a 5.", "param2");
   25     }
   26 }
   27
   28 [TestMethod]
   29 public void MetodoConParametros_falla_si_param1_es_vacio() {
   30
   31     var ex = CustomAssert.Throws<ArgumentException>(() => MetodoConParametros("", 4));
   32
   33     Assert.AreEqual<string>("param1", ex.ParamName, "Se esperaba que param1 fuese el parámetro con problemas.");
   34 }
   35
   36 [TestMethod]
   37 public void MetodoConParametros_falla_si_param2_es_mayor_a_5() {
   38
   39     var ex = CustomAssert.Throws<ArgumentException>(() => MetodoConParametros("test", 80));
   40
   41     Assert.AreEqual<string>("param2", ex.ParamName, "Se esperaba que param2 fuese el parámetro con problemas.");
   42 }

Bien, eso es todo por el momento, espero que esta clase les sea tan útil como me resulta a mi (ya forma parte de mi ‘core’ para testing).

Pueden bajar la clase CustomAsserts aquí.

¡Nos vemos en el próximo post!

Publicado originalmente en https://gerardocontijoch.wordpress.com.

Posted in Testing | Etiquetado: , , , , | 3 Comments »