Il modo migliore per testare le eccezioni con Assert per assicurarsi che vengano lanciate


97

Pensi che questo sia un buon modo per testare le eccezioni? Eventuali suggerimenti?

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

Assert.IsNotNull(exception);

Sto usando MS Test.

Risposte:


137

Ho un paio di modelli diversi che uso. Uso l' ExpectedExceptionattributo la maggior parte delle volte quando è prevista un'eccezione. Ciò è sufficiente per la maggior parte dei casi, tuttavia, in alcuni casi ciò non è sufficiente. L'eccezione potrebbe non essere catturabile, poiché viene lanciata da un metodo invocato per riflessione, o forse voglio solo verificare che siano presenti altre condizioni, ad esempio una transazione viene annullata o un valore è ancora stato impostato. In questi casi lo racchiudo in un try/catchblocco che si aspetta l'eccezione esatta, esegue un'eccezione Assert.Failse il codice ha esito positivo e rileva anche eccezioni generiche per assicurarsi che non venga generata un'eccezione diversa.

Primo caso:

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

Secondo 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 )
        );
    }
}

16
Molti framework di unit test implementano gli errori di asserzione come eccezioni. Quindi Assert.Fail () nel secondo caso verrà catturato dal blocco catch (Exception), che nasconderà il messaggio di eccezione. Devi aggiungere una cattura (NUnit.Framework.AssertionException) {throw;} o simile - vedi la mia risposta.
GrahamS

@Graham - L'ho digitato nella parte superiore della testa. Di solito stamperei anche il messaggio di eccezione oltre al suo tipo. Il punto è che il test fallirebbe poiché il secondo gestore rileverà il fallimento dell'asserzione e "rifarebbe" le informazioni sull'errore.
tvanfosson

1
Sebbene il tuo codice sia funzionalmente valido, non consiglio di utilizzare l'attributo ExpectedException (poiché è troppo vincolante e soggetto a errori) o di scrivere un blocco try / catch in ogni test (poiché è troppo complicato e soggetto a errori). Usa un metodo di asserzione ben progettato, fornito dal tuo framework di test o scrivi il tuo. Puoi ottenere un codice migliore e non dovrai scegliere tra le diverse tecniche o passare da una all'altra man mano che il test cambia. Vedi stackoverflow.com/a/25084462/2166177
steve

Cordiali saluti, sono passato all'uso di xUnit che ha un Assert.Throwsmetodo fortemente tipizzato che copre entrambi questi casi.
tvanfosson

L'attributo ExpectedException è un modo antiquato e datato per verificare se vengono generate eccezioni. Vedi la mia risposta completa di seguito.
bytedev

43

Ora, 2017, puoi farlo più facilmente con il nuovo MSTest V2 Framework :

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

//async version
await Assert.ThrowsExceptionAsync<SomeException>(
  () => myObject.SomeMethodAsync()
);

Questo avrà successo solo se System.Exceptionviene lanciato un. Qualsiasi altro, come un System.ArgumentException, fallirà il test.
sschoof

2
Se ti aspetti un altro tipo di eccezione, dovresti testarlo ... Nel tuo esempio, dovresti fare: Assert.ThrowsException <ArgumentException> (() => myClass.MyMethodWithError ());
Icaro Bombonato

2
Qualcosa di importante da notare è che l'uso di Assert.ThrowsException<MyException>testerà esclusivamente rispetto al tipo di eccezione fornito e non a nessuno dei suoi tipi di eccezione derivati. Nel mio esempio, se il test Subfosse su Throwun MyInheritedException(tipo derivato dalla classe base MyException), il test fallirebbe .
Ama

Se vuoi ampliare il tuo test e accettare un tipo di eccezione oltre ai suoi tipi derivati, usa un file Try { SubToTest(); Assert.Fail("...") } Catch (AssertFailedException e) {throw;} Catch (MyException e) {...}. Notare la massima importanza di Catch (AssertFailedException e) {throw;}(cfr. Commento di allgeek)
Ama

17

Sono nuovo qui e non ho la reputazione di commentare o votare in negativo, ma volevo sottolineare un difetto nell'esempio nella risposta di Andy White :

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

In tutti i framework di unit test che conosco, Assert.Failfunziona generando un'eccezione, quindi il catch generico maschererà effettivamente il fallimento del test. Se SomethingThatCausesAnException()non lancia, la Assert.Failvolontà, ma questo non verrà mai fuoriuscire dal test runner per indicare il fallimento.

Se è necessario catturare l'eccezione attesa (cioè, per affermare determinati dettagli, come il messaggio / le proprietà sull'eccezione), è importante catturare il tipo atteso specifico e non la classe di eccezione di base. Ciò consentirebbe la Assert.Failfuoriuscita dell'eccezione (supponendo che non si stia generando lo stesso tipo di eccezione del framework di unit test), ma consentire comunque la convalida sull'eccezione generata dal SomethingThatCausesAnException()metodo.


15

A partire dalla v 2.5, NUnit ha i seguenti livelli di metodo Assertper testare le eccezioni:

Assert.Throws , che testerà un tipo di eccezione esatto:

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

E Assert.Catch, che testerà un'eccezione di un determinato tipo o un tipo di eccezione derivato da questo tipo:

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

Per inciso, durante il debug di unit test che generano eccezioni, potresti voler impedire a VS di interrompere l'eccezione .

modificare

Giusto per fare un esempio del commento di Matteo di seguito, il ritorno del generico Assert.Throwsed Assert.Catchè l'eccezione con il tipo di eccezione, che puoi poi esaminare per un ulteriore controllo:

// The type of ex is that of the generic type parameter (SqlException)
var ex = Assert.Throws<SqlException>(() => MethodWhichDeadlocks());
Assert.AreEqual(1205, ex.Number);

2
Roy Osherove lo raccomanda in The Art of Unit Testing, seconda ed, sezione 2.6.2.
Avi

2
Mi piace Assert.Throws, inoltre restituisce l'eccezione in modo da poter scrivere ulteriori asserzioni sull'eccezione stessa.
Matteo

La domanda era per MSTest e non per NUnit.
bytedev

La domanda originale di @nashwan OP non aveva quella qualifica e il tagging non qualifica ancora MS-Test. Allo stato attuale, è una domanda C #, .Net, Unit-Testing.
StuartLC

11

Sfortunatamente MSTest ANCORA ha davvero solo l'attributo ExpectedException (mostra solo quanto MS si preoccupa di MSTest) che IMO è piuttosto orribile perché rompe il modello Arrange / Act / Assert e non ti consente di specificare esattamente quale riga di codice ti aspetti l'eccezione a verificarsi.

Quando utilizzo (/ costretto da un client) per utilizzare MSTest, utilizzo sempre questa classe di supporto:

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.");
    }
}

Esempio di utilizzo:

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

10

In alternativa all'utilizzo di ExpectedExceptionattributi, a volte definisco due metodi utili per le mie classi di test:

AssertThrowsException() accetta un delegato e afferma che genera l'eccezione prevista con il messaggio previsto.

AssertDoesNotThrowException() accetta lo stesso delegato e afferma che non genera un'eccezione.

Questa associazione può essere molto utile quando si desidera verificare che un'eccezione venga generata in un caso, ma non nell'altro.

Usandoli il mio codice di unit test potrebbe essere simile a questo:

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);

Bello e pulito eh?

My AssertThrowsException()e AssertDoesNotThrowException()metodi sono definiti su una classe base comune come segue:

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);
    }
}

4

Contrassegnare il test con ExpectedExceptionAttribute (questo è il termine in NUnit o MSTest; gli utenti di altri framework di unit test potrebbero dover tradurre).


Non utilizzare ExpectedExceptionAttribute (motivo fornito nel mio post di seguito). NUnit ha Assert.Throws <YourException> () e per MSTest usa qualcosa come la mia classe AssertException di seguito.
bytedev

3

Con la maggior parte dei framework di unit test .net è possibile inserire un attributo [ExpectedException] nel metodo di test. Tuttavia, questo non può dirti che l'eccezione è avvenuta nel punto in cui ti aspettavi. È qui che xunit.net può aiutarti.

Con xunit hai Assert.Throws, quindi puoi fare cose come questa:

    [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));
    }

[Fact] è l'equivalente xunit di [TestMethod]


Se è necessario utilizzare MSTest (a cui sono costretto spesso dai datori di lavoro), vedere la mia risposta di seguito.
bytedev

0

Suggerire di utilizzare la sintassi del delegato pulito di NUnit .

Esempio per il test 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>);
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.