Gerardo Contijoch

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

Posts Tagged ‘excepciones’

La importancia del uso de las excepciones adecuadas

Posted by Gerardo Contijoch en enero 2, 2009

Si hay algo que me gusta, eso es lanzar excepciones. Ojo, no es que me guste provocarlas (aunque puedo ser muy dañino durante la etapa de testing… hehe) sino que lo que me gusta es pensar en como y cuando lanzarlas. Me encanta pensar y razonar sobre que excepción es la más adecuada para cada caso, si hay que utilizar una ya existente o hay que crear una nueva, que mensaje asociarle a la excepción, atraparlas y relanzarlas, etc. Es como un juego en donde se pasa una pelota (¿PelotaException? :P) y solo se la tiene que quedar su dueño. Y no siempre es un juego fácil, hay que saber atrapar y lanzar la pelota o esta puede terminar en cualquier lado.

Hace unos días me encontré con un post de Jared Parsons, en el que explicaba el (aparentemente) correcto uso de las excepciones NotSupportedException y NotImplementedException. Básicamente la diferencia principal entre estas dos excepciones es que la primera se lanza cuando una clase no implementa un miembro y es posible determinar si el mismo esta o no implementado (en otras palabras, se sabe de antemano que el miembro en cuestión no esta implementado); mientras que la segunda se lanza cuando el miembro no esta implementado por cualquier otro motivo. A mi no me convence mucho ese ‘cualquier otro motivo’ ya que el único caso, además del presentado anteriormente, en el que no se implementa un método es el caso de que el código no esté terminado y al decir ‘cualquier otro motivo’ parece que se está dando una excusa para lanzar esa excepción cuando queramos.

Sumado al caso anterior, tenemos las excepciones ArgumentException, ArgumentNullException y ArgumentOutOfRangeException, todas muy similares.

Veamos el siguiente código:

   1: public void AgregarCompraAOrden(Orden o, Producto p, int cantidad){
   2:     if(o == null || p == null){
   3:         throw new ArgumentNullException(o == null ? "o" : "p");
   4:     }
   5:
   6:     if(o.EstaCerrada){
   7:         throw new ArgumentException("La orden debe estar abierta para que se le puedan agregar productos.", "o");
   8:     }
   9:
  10:     if(cantidad <= 0){
  11:         throw new ArgumentOutOfRangeException("cantidad", "La cantidad debe ser mayor a 0.");
  12:     }
  13:
  14:     /*...*/
  15: }

Supongamos que en todos los casos anteriores solo lanzamos ArgumentException (debido a que hay un problema con uno de los argumentos o parámetros del método). Nos sería imposible determinar cual es el problema sin leer el mensaje. La diferencia entre estas tres excepciones es mínima, pero el uso correcto de las mismas facilita enormemente la depuración del código.

No siempre es sencillo determinar la excepción a lanzar. Lo ideal siempre es utilizar las excepciones que ya vienen en el framework, pero muchas veces la respuesta esta en crear una nueva excepción únicamente para una situación particular. Puede parecer mucho trabajo para muy poco, pero no lo es. Veamos otro ejemplo:

   1: public void AgregarCompraAOrden(Orden o, Producto p, int cantidad, int descuento){
   2:
   3:     /* Validaciones de producto y orden */
   4:
   5:     if(cantidad <= 0){
   6:         throw new ArgumentOutOfRangeException("cantidad", "La cantidad debe ser mayor a 0.");
   7:     }
   8:
   9:     if(descuento < 0 || descuento > 100){
  10:         throw new DescuentoInvalidoException("El descuento no puede ser negativo ni mayor al 100%");
  11:     }
  12:
  13:     /*...*/
  14: }

En este caso la validación del descuento no lanza ArgumentOutOfRangeException (que seria la excepción esperada) sino una excepción propia que sólo se utiliza para indicar valores de descuento inválidos y nada más. Una vez más, la diferencia es mínima, pero el significado de utilizar ArgumentOutOfRangeException o DescuentoInvalidoException es muy diferente y nuestra lógica puede llegar a procesar las ordenes de manera diferente dependiendo de que excepción se lance.

Cabe aclarar una cosa en el caso anterior. Se creó una excepción nueva porque un producto con 0 como cantidad implica que no se agrega un producto (lo cual no tiene sentido dentro del método AgregarCompraAOrden), pero un descuento inválido puede significar que no se aplican descuentos o que la orden no se procesa hasta que no pase una revisión o cualquier otra cosa. Si para nosotros un descuento inválido es lo mismo que una cantidad inválida (es decir, un error en un número que evita que se procese la orden) entonces habría que considerar usar ArgumentOutOfRangeException en ambos casos debido a que no se espera que los mismos se procesen de manera diferente. La idea de usar distintas excepciones es informar distintas cosas, pero si para nosotros cualquier error es lo mismo (rara vez debería ser así…), entonces habría que usar las mismas excepciones ya que no nos interesa la información extra que nos puedan proveer los distintos tipos.

¡Nos vemos en el próximo post!

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

Posted in .Net 2.0, Diseño | 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 »