La mejor manera de probar excepciones con Assert para asegurarse de que se lanzarán

97

¿Crees que esta es una buena forma de probar excepciones? ¿Alguna sugerencia?

Exception exception = null;
try{
    //I m sure that an exeption will happen here
}
catch (Exception ex){
    exception = ex;
}

Assert.IsNotNull(exception);

Estoy usando MS Test.

Hannoun Yassir
fuente

Respuestas:

137

Tengo un par de patrones diferentes que utilizo. Utilizo el ExpectedExceptionatributo la mayor parte del tiempo cuando se espera una excepción. Esto es suficiente para la mayoría de los casos, sin embargo, hay algunos casos en los que esto no es suficiente. Es posible que la excepción no se pueda detectar, ya que es lanzada por un método que se invoca por reflexión, o tal vez solo quiero verificar que se cumplan otras condiciones, digamos que una transacción se revierte o que aún se ha establecido algún valor. En estos casos, lo envuelvo en un try/catchbloque que espera la excepción exacta, hace un Assert.Failsi el código tiene éxito y también detecta excepciones genéricas para asegurarme de que no se lanza una excepción diferente.

Primer caso:

[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void MethodTest()
{
     var obj = new ClassRequiringNonNullParameter( null );
}

Segundo caso:

[TestMethod]
public void MethodTest()
{
    try
    {
        var obj = new ClassRequiringNonNullParameter( null );
        Assert.Fail("An exception should have been thrown");
    }
    catch (ArgumentNullException ae)
    {
        Assert.AreEqual( "Parameter cannot be null or empty.", ae.Message );
    }
    catch (Exception e)
    {
        Assert.Fail(
             string.Format( "Unexpected exception of type {0} caught: {1}",
                            e.GetType(), e.Message )
        );
    }
}
tvanfosson
fuente
16
Muchos marcos de pruebas unitarias implementan fallas de aserción como excepciones. Entonces, el Assert.Fail () en el segundo caso quedará atrapado por el bloque catch (Exception), que ocultará el mensaje de excepción. Debe agregar una captura (NUnit.Framework.AssertionException) {throw;} o similar; vea mi respuesta.
GrahamS
@Graham: escribí esto en la parte superior de mi cabeza. Por lo general, también imprimiría el mensaje de excepción además de su tipo. El punto es que la prueba fallaría ya que el segundo controlador detectaría el error de aserción y "volvería a fallar" con información sobre el error.
tvanfosson
1
Aunque su código es funcionalmente sólido, no recomiendo usar el atributo ExpectedException (ya que es demasiado restrictivo y propenso a errores) o escribir un bloque try / catch en cada prueba (ya que es demasiado complicado y propenso a errores). Utilice un método de aserción bien diseñado, ya sea proporcionado por su marco de prueba o escriba el suyo propio. Puede lograr un mejor código y no tendrá que elegir entre las diferentes técnicas o cambiar de una a otra a medida que cambia la prueba. Ver stackoverflow.com/a/25084462/2166177
steve
FYI: he pasado a usar xUnit, que tiene un Assert.Throwsmétodo fuertemente tipado que cubre ambos casos.
tvanfosson
El atributo ExpectedException es una forma desagradable y anticuada de probar si se lanzan excepciones. Vea mi respuesta completa a continuación.
bytedev
42

Ahora, 2017, puede hacerlo más fácil con el nuevo marco MSTest V2 :

Assert.ThrowsException<Exception>(() => myClass.MyMethodWithError());

//async version
await Assert.ThrowsExceptionAsync<SomeException>(
  () => myObject.SomeMethodAsync()
);
Icaro Bombonato
fuente
Esto solo tendrá éxito si System.Exceptionse lanza un. Cualquier otro, como un System.ArgumentException, fallará la prueba.
sschoof
2
Si está esperando otro tipo de excepción, debe probarlo ... En su ejemplo, debe hacer: Assert.ThrowsException <ArgumentException> (() => myClass.MyMethodWithError ());
Icaro Bombonato
2
Algo importante a tener en cuenta es que el uso de Assert.ThrowsException<MyException>se probará únicamente con el tipo de excepción proporcionado, y no con ninguno de sus tipos de excepción derivados. En mi ejemplo, si la prueba Subfue a Throwun MyInheritedException(tipo derivado de la clase base MyException), la prueba fallaría .
Ama
Si desea ampliar su prueba y aceptar un tipo de excepción, así como sus tipos derivados, use un Try { SubToTest(); Assert.Fail("...") } Catch (AssertFailedException e) {throw;} Catch (MyException e) {...}. Tenga en cuenta la mayor importancia de Catch (AssertFailedException e) {throw;}(cf. comentario de allgeek)
Ama
17

Soy nuevo aquí y no tengo la reputación de comentar o votar en contra, pero quería señalar una falla en el ejemplo en la respuesta de Andy White :

try
{
    SomethingThatCausesAnException();
    Assert.Fail("Should have exceptioned above!");
}
catch (Exception ex)
{
    // whatever logging code
}

En todos los marcos de pruebas unitarias con los que estoy familiarizado, Assert.Failfunciona lanzando una excepción, por lo que la captura genérica enmascarará la falla de la prueba. Si SomethingThatCausesAnException()no lanza, lo Assert.Failhará, pero eso nunca saldrá a la luz al corredor de prueba para indicar una falla.

Si necesita capturar la excepción esperada (es decir, afirmar ciertos detalles, como el mensaje / propiedades de la excepción), es importante capturar el tipo esperado específico, y no la clase de excepción base. Eso permitiría que surgiera la Assert.Failexcepción (asumiendo que no está lanzando el mismo tipo de excepción que hace su marco de prueba unitaria), pero aún así permitiría la validación de la excepción lanzada por su SomethingThatCausesAnException()método.

allgeek
fuente
15

A partir de la versión 2.5, NUnit tiene los siguientes niveles de método Assertpara probar excepciones:

Assert.Throws , que probará un tipo de excepción exacto:

Assert.Throws<NullReferenceException>(() => someNullObject.ToString());

Y Assert.Catch, que probará una excepción de un tipo dado, o un tipo de excepción derivado de este tipo:

Assert.Catch<Exception>(() => someNullObject.ToString());

Además, al depurar pruebas unitarias que arrojan excepciones, es posible que desee evitar que VS se rompa en la excepción .

Editar

Solo para dar un ejemplo del comentario de Matthew a continuación, el retorno del genérico Assert.Throwsy Assert.Catches la excepción con el tipo de excepción, que luego puede examinar para una inspección más detallada:

// The type of ex is that of the generic type parameter (SqlException)
var ex = Assert.Throws<SqlException>(() => MethodWhichDeadlocks());
Assert.AreEqual(1205, ex.Number);
StuartLC
fuente
2
Roy Osherove recomienda esto en The Art of Unit Testing, segunda edición, sección 2.6.2.
Avi
2
Me gusta Assert.Throws, además, devuelve la excepción para que pueda escribir más afirmaciones sobre la propia excepción.
Mateo
La pregunta era para MSTest, no para NUnit.
bytedev
La pregunta original de @nashwan OP no tenía esa calificación, y el etiquetado aún no califica para MS-Test. Tal como está, es una pregunta de prueba unitaria de C #, .Net.
StuartLC
11

Desafortunadamente, MSTest TODAVÍA solo tiene el atributo ExpectedException (solo muestra cuánto se preocupa MS por MSTest), lo que IMO es bastante terrible porque rompe el patrón Arrange / Act / Assert y no le permite especificar exactamente qué línea de código espera la excepción que ocurra en.

Cuando estoy usando (/ forzado por un cliente) para usar MSTest, siempre uso esta clase auxiliar:

public static class AssertException
{
    public static void Throws<TException>(Action action) where TException : Exception
    {
        try
        {
            action();
        }
        catch (Exception ex)
        {
            Assert.IsTrue(ex.GetType() == typeof(TException), "Expected exception of type " + typeof(TException) + " but type of " + ex.GetType() + " was thrown instead.");
            return;
        }
        Assert.Fail("Expected exception of type " + typeof(TException) + " but no exception was thrown.");
    }

    public static void Throws<TException>(Action action, string expectedMessage) where TException : Exception
    {
        try
        {
            action();
        }
        catch (Exception ex)
        {
            Assert.IsTrue(ex.GetType() == typeof(TException), "Expected exception of type " + typeof(TException) + " but type of " + ex.GetType() + " was thrown instead.");
            Assert.AreEqual(expectedMessage, ex.Message, "Expected exception with a message of '" + expectedMessage + "' but exception with message of '" + ex.Message + "' was thrown instead.");
            return;
        }
        Assert.Fail("Expected exception of type " + typeof(TException) + " but no exception was thrown.");
    }
}

Ejemplo de uso:

AssertException.Throws<ArgumentNullException>(() => classUnderTest.GetCustomer(null));
bytedev
fuente
10

Como alternativa al uso de ExpectedExceptionatributos, a veces defino dos métodos útiles para mis clases de prueba:

AssertThrowsException() toma un delegado y afirma que lanza la excepción esperada con el mensaje esperado.

AssertDoesNotThrowException() toma el mismo delegado y afirma que no lanza una excepción.

Este emparejamiento puede resultar muy útil cuando desee probar que se lanza una excepción en un caso, pero no en el otro.

Al usarlos, mi código de prueba unitaria podría verse así:

ExceptionThrower callStartOp = delegate(){ testObj.StartOperation(); };

// Check exception is thrown correctly...
AssertThrowsException(callStartOp, typeof(InvalidOperationException), "StartOperation() called when not ready.");

testObj.Ready = true;

// Check exception is now not thrown...
AssertDoesNotThrowException(callStartOp);

Agradable y ordenado, ¿eh?

Mis métodos AssertThrowsException()y AssertDoesNotThrowException()se definen en una clase base común de la siguiente manera:

protected delegate void ExceptionThrower();

/// <summary>
/// Asserts that calling a method results in an exception of the stated type with the stated message.
/// </summary>
/// <param name="exceptionThrowingFunc">Delegate that calls the method to be tested.</param>
/// <param name="expectedExceptionType">The expected type of the exception, e.g. typeof(FormatException).</param>
/// <param name="expectedExceptionMessage">The expected exception message (or fragment of the whole message)</param>
protected void AssertThrowsException(ExceptionThrower exceptionThrowingFunc, Type expectedExceptionType, string expectedExceptionMessage)
{
    try
    {
        exceptionThrowingFunc();
        Assert.Fail("Call did not raise any exception, but one was expected.");
    }
    catch (NUnit.Framework.AssertionException)
    {
        // Ignore and rethrow NUnit exception
        throw;
    }
    catch (Exception ex)
    {
        Assert.IsInstanceOfType(expectedExceptionType, ex, "Exception raised was not the expected type.");
        Assert.IsTrue(ex.Message.Contains(expectedExceptionMessage), "Exception raised did not contain expected message. Expected=\"" + expectedExceptionMessage + "\", got \"" + ex.Message + "\"");
    }
}

/// <summary>
/// Asserts that calling a method does not throw an exception.
/// </summary>
/// <remarks>
/// This is typically only used in conjunction with <see cref="AssertThrowsException"/>. (e.g. once you have tested that an ExceptionThrower
/// method throws an exception then your test may fix the cause of the exception and then call this to make sure it is now fixed).
/// </remarks>
/// <param name="exceptionThrowingFunc">Delegate that calls the method to be tested.</param>
protected void AssertDoesNotThrowException(ExceptionThrower exceptionThrowingFunc)
{
    try
    {
        exceptionThrowingFunc();
    }
    catch (NUnit.Framework.AssertionException)
    {
        // Ignore and rethrow any NUnit exception
        throw;
    }
    catch (Exception ex)
    {
        Assert.Fail("Call raised an unexpected exception: " + ex.Message);
    }
}
GrahamS
fuente
4

Marque la prueba con ExpectedExceptionAttribute (este es el término en NUnit o MSTest; los usuarios de otros marcos de prueba unitarios pueden necesitar traducir).

itowlson
fuente
No use ExpectedExceptionAttribute (razón dada en mi publicación a continuación). NUnit tiene Assert.Throws <YourException> () y para MSTest use algo como mi clase AssertException a continuación.
bytedev
3

Con la mayoría de los marcos de prueba unitarios .net, puede poner un atributo [ExpectedException] en el método de prueba. Sin embargo, esto no puede decirle que la excepción ocurrió en el punto que esperaba. Ahí es donde xunit.net puede ayudar.

Con xunit tienes Assert.Throws, por lo que puedes hacer cosas como esta:

    [Fact]
    public void CantDecrementBasketLineQuantityBelowZero()
    {
        var o = new Basket();
        var p = new Product {Id = 1, NetPrice = 23.45m};
        o.AddProduct(p, 1);
        Assert.Throws<BusinessException>(() => o.SetProductQuantity(p, -3));
    }

[Hecho] es el equivalente xunit de [TestMethod]

Steve Willcock
fuente
Si debe usar MSTest (que los empleadores me obligan a menudo), consulte mi respuesta a continuación.
bytedev
0

Sugiera usar la sintaxis de delegado limpia de NUnit .

Ejemplo de prueba ArgumentNullExeption:

[Test]
[TestCase(null)]
public void FooCalculation_InvalidInput_ShouldThrowArgumentNullExeption(string text)
{
    var foo = new Foo();
    Assert.That(() => foo.Calculate(text), Throws.ArgumentNullExeption);

    //Or:
    Assert.That(() => foo.Calculate(text), Throws.Exception.TypeOf<ArgumentNullExeption>);
}
Shahar Shokrani
fuente