Come si usa Assert per verificare che è stata generata un'eccezione?


830

Come si utilizza Assert(o altra classe di test?) Per verificare che sia stata generata un'eccezione?


Quale framework di test unitari stai usando?
Kevin Pullin,

3
Visual Studio integrato
Alex

4
L'attributo ExpectedException non aiuta? ref: msdn.microsoft.com/en-us/library/...
shahkalpesh

2
Divertente, ho appena finito di cercare la risposta a questo, l'ho trovata su stackoverflow.com/questions/741029/testing-exceptions .
dfjacobs,

Risposte:


978

Per "Visual Studio Team Test" sembra che si applichi l'attributo ExpectedException al metodo del test.

Esempio dalla documentazione qui: A Test unit Unit Soluzione con Visual Studio Team Test

[TestMethod]
[ExpectedException(typeof(ArgumentException),
    "A userId of null was inappropriately allowed.")]
public void NullUserIdInConstructor()
{
   LogonInfo logonInfo = new LogonInfo(null, "P@ss0word");
}

25
L'attributo ExpectedException sopra funziona anche in NUnit (ma [TestMethod] dovrebbe essere [Test]).
dbkk,

5
@dbkk: non funziona esattamente allo stesso modo in NUnit - il messaggio viene trattato come una stringa che deve matcvh il messaggio di eccezione (e IU pensa che abbia più senso)
Ruben Bartelink,

29
Questo attributo esegue il lavoro ed è una funzionalità integrata per i programmatori c #, ma non è consigliabile utilizzarlo poiché non è abbastanza flessibile. Considera cosa succede se il tipo di eccezione viene generato dal codice di configurazione del test: il test passa, ma in realtà non ha fatto ciò che ti aspettavi. O cosa succede se si desidera verificare lo stato dell'oggetto eccezione. Di solito voglio usare StringAssert.Contains (e.Message ...) piuttosto che testare l'intero messaggio. Utilizzare un metodo assert come descritto in altre risposte.
steve

3
Evitare di utilizzare ExpectedException in NUnit, poiché verrà eliminato in NUnit 3.0. Preferisco usare Assert.Throws <SpecificException> ()
Terence

5
È possibile utilizzare Assert.ThrowsException <T> e Assert.ThrowsExceptionAsync <T> in MsTest.
Gopal Krishnan,

257

Di solito il tuo framework di test avrà una risposta per questo. Ma se non è abbastanza flessibile, puoi sempre farlo:

try {
    somethingThatShouldThrowAnException();
    Assert.Fail(); // If it gets to this line, no exception was thrown
} catch (GoodException) { }

Come sottolinea @Jonas, questo NON funziona per rilevare un'eccezione di base:

try {
    somethingThatShouldThrowAnException();
    Assert.Fail(); // raises AssertionException
} catch (Exception) {
    // Catches the assertion exception, and the test passes
}

Se è assolutamente necessario rilevare l'eccezione, è necessario ricodificare Assert.Fail (). Ma davvero, questo è un segno che non dovresti scriverlo a mano; controlla le opzioni del tuo framework di test o vedi se riesci a lanciare un'eccezione più significativa da testare.

catch (AssertionException) { throw; }

Dovresti essere in grado di adattare questo approccio a quello che ti piace, incluso specificare quali tipi di eccezioni catturare. Se ti aspetti solo alcuni tipi, finisci i catchblocchi con:

} catch (GoodException) {
} catch (Exception) {
    // not the right kind of exception
    Assert.Fail();
}

20
+1, uso questo modo invece dell'attributo quando devo fare affermazioni oltre il solo tipo di eccezione. Ad esempio, cosa succede se è necessario verificare che determinati campi nell'istanza dell'eccezione siano impostati su determinati valori.
Pavel Repin,

2
Non è necessario specificare il messaggio di errore. Questo è sufficiente: [ExpectedException (typeof (ArgumentException))]
mibollma,

5
Penso che questa soluzione sia la migliore. [ExpectedException (typeof (ArgumentException))] ha i suoi usi, se il test è semplice, ma è a mio avviso una soluzione pigra ed essere a proprio agio può portare a insidie. Questa soluzione offre un controllo specifico per eseguire un test più corretto, oltre a un test Writeline per il rapporto di esecuzione del test, che l'eccezione è stata effettivamente generata come previsto.
pesce cattivo

12
Fai attenzione perché Assert.Fail () solleva un'eccezione, se la prendi, il test passa!
Jonas,

4
@ Vinnyq12 Quello che voglio dire è che il primo test nell'esempio sopra non fallirà mai. Un test fallisce se viene generata un'eccezione (e non "catturata" da ExpectedExceptionAttribute)
Jonas

113

Il mio metodo preferito per implementare questo è scrivere un metodo chiamato Throws e usarlo come qualsiasi altro metodo Assert. Sfortunatamente, .NET non consente di scrivere un metodo di estensione statica, quindi non è possibile utilizzare questo metodo come se appartenesse effettivamente alla build nella classe Assert; basta crearne un altro chiamato MyAssert o qualcosa di simile. La classe si presenta così:

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace YourProject.Tests
{
    public static class MyAssert
    {
        public static void Throws<T>( Action func ) where T : Exception
        {
            var exceptionThrown = false;
            try
            {
                func.Invoke();
            }
            catch ( T )
            {
                exceptionThrown = true;
            }

            if ( !exceptionThrown )
            {
                throw new AssertFailedException(
                    String.Format("An exception of type {0} was expected, but not thrown", typeof(T))
                    );
            }
        }
    }
}

Ciò significa che il test unitario è simile al seguente:

[TestMethod()]
public void ExceptionTest()
{
    String testStr = null;
    MyAssert.Throws<NullReferenceException>(() => testStr.ToUpper());
}

Che assomiglia e si comporta in modo molto più simile al resto delle sintassi del test unitario.


1
Sbarazzati della bandiera bool e metti il ​​tiro sulla linea direttamente dopo l'invocazione per un'implementazione più compatta.
GT

11
L'unica cosa che lo rende migliore è che la funzione restituisca l'eccezione rilevata in modo da poter continuare ad affermare che cose come gli attributi sull'eccezione sono corrette.
Mark Hildreth,

2
Grazie! Questo sembra il miglior approccio per me perché è un modo breve per testare più eccezioni in un metodo. È anche molto più leggibile.
David Sherret,

2
@MickeyPerlstein Gli attributi infrangono le regole AAA per i test. In particolare, se il tuo Arrange lancia l'eccezione prima ancora di arrivare all'Atto, allora il tuo test passa ... eek!
freedomn-m,

2
Microsoft ha finalmente iniziato ad aggiornare MSTest - v2 supporta Assert.ThrowsException<T>e Assert.ThrowsExceptionAsync<T>- vedi blogs.msdn.microsoft.com/visualstudioalm/2017/02/25/…
Quango

62

se usi NUNIT, puoi fare qualcosa del genere:

Assert.Throws<ExpectedException>(() => methodToTest());


È anche possibile memorizzare l'eccezione generata per convalidarla ulteriormente:

ExpectedException ex = Assert.Throws<ExpectedException>(() => methodToTest());
Assert.AreEqual( "Expected message text.", ex.Message );
Assert.AreEqual( 5, ex.SomeNumber);

Vedi: http://nunit.org/docs/2.5/exceptionAsserts.html


60

Se stai usando MSTest, che in origine non aveva un ExpectedExceptionattributo, potresti farlo:

try 
{
    SomeExceptionThrowingMethod()
    Assert.Fail("no exception thrown");
}
catch (Exception ex)
{
    Assert.IsTrue(ex is SpecificExceptionType);
}

3
Questo funziona, ma non lo consiglio in generale poiché la logica è eccessivamente complicata. Non dire che è contorto, ma considera se scrivi questo blocco di codice per più test - 10, 100s di test. Questa logica deve essere sviluppata secondo un metodo di assert ben progettato. Vedi altre risposte
steve

35

Diffidare di utilizzare ExpectedException, in quanto può portare a diverse insidie, come dimostrato qui:

http://geekswithblogs.net/sdorman/archive/2009/01/17/unit-testing-and-expected-exceptions.aspx

E qui:

http://xunit.github.io/docs/comparisons.html

Se hai bisogno di verificare le eccezioni, ci sono meno modi corretti. È possibile utilizzare il metodo try {act / fail} catch {assert}, che può essere utile per i framework che non dispongono di supporto diretto per i test delle eccezioni diversi da ExpectedException.

Un'alternativa migliore è usare xUnit.NET, che è un framework di test di unità molto moderno, lungimirante ed estensibile che ha imparato da tutti gli altri errori e migliorato. Uno di questi miglioramenti è Assert.Throws, che fornisce una sintassi molto migliore per affermare le eccezioni.

Puoi trovare xUnit.NET su github: http://xunit.github.io/


4
Nota che NUnit 2.5 supporta anche Assert. Ora lancia anche la sintassi dello stile - nunit.com/index.php?p=releaseNotes&r=2.5
Alconja

Il modo in cui i test unitari si interrompono per informarti dell'eccezione quando si utilizza ExpectedException mi fa impazzire. Perché MS ha pensato che fosse una buona idea fare un passo manuale nei test automatizzati? Grazie per i collegamenti.
Formica

@Ant: MS ha copiato NUnit ... quindi la vera domanda è: perché NUnit ha pensato che fosse una buona idea?
jrista,

28

MSTest (v2) ora ha una funzione Assert.ThrowsException che può essere utilizzata in questo modo:

Assert.ThrowsException<System.FormatException>(() =>
            {
                Story actual = PersonalSite.Services.Content.ExtractHeader(String.Empty);
            }); 

Puoi installarlo con nuget: Install-Package MSTest.TestFramework


Nel 2018 questa è considerata la migliore pratica poiché controlla solo l'unità in fase di test e non qualche altro codice.
CM

24

In un progetto a cui sto lavorando abbiamo un'altra soluzione per farlo.

Innanzitutto non mi piace ExpectedExceptionAttribute perché prende in considerazione la chiamata del metodo che ha causato l'eccezione.

Lo faccio invece con un helpermethod.

Test

[TestMethod]
public void AccountRepository_ThrowsExceptionIfFileisCorrupt()
{
     var file = File.Create("Accounts.bin");
     file.WriteByte(1);
     file.Close();

     IAccountRepository repo = new FileAccountRepository();
     TestHelpers.AssertThrows<SerializationException>(()=>repo.GetAll());            
}

HelperMethod

public static TException AssertThrows<TException>(Action action) where TException : Exception
    {
        try
        {
            action();
        }
        catch (TException ex)
        {
            return ex;
        }
        Assert.Fail("Expected exception was not thrown");

        return null;
    }

Pulito, non è vero;)


14

È un attributo sul metodo di test ... non usi Assert. Somiglia a questo:

[ExpectedException(typeof(ExceptionType))]
public void YourMethod_should_throw_exception()

13

Puoi scaricare un pacchetto da Nuget usando: PM> Installa-pacchetto MSTestExtensions che aggiunge Assert.Throws () sintassi nello stile di nUnit / xUnit a MsTest.

Istruzioni di alto livello: scarica l'assembly ed eredita da BaseTest e puoi usare Assert.Throws () sintassi .

Il metodo principale per l'implementazione di Throws è il seguente:

public static void Throws<T>(Action task, string expectedMessage, ExceptionMessageCompareOptions options) where T : Exception
{
    try
    {
        task();
    }
    catch (Exception ex)
    {
        AssertExceptionType<T>(ex);
        AssertExceptionMessage(ex, expectedMessage, options);
        return;
    }

    if (typeof(T).Equals(new Exception().GetType()))
    {
        Assert.Fail("Expected exception but no exception was thrown.");
    }
    else
    {
        Assert.Fail(string.Format("Expected exception of type {0} but no exception was thrown.", typeof(T)));
    }
}

Divulgazione: ho messo insieme questo pacchetto.

Ulteriori informazioni: http://www.bradoncode.com/blog/2012/01/asserting-exceptions-in-mstest-with.html


Grazie per l'esempio Hai un esempio di come testare un Assert.DoesNotThrow () o equivalente?
Lane Goolsby,

10

Puoi raggiungerlo con una semplice riga.

Se l'operazione foo.bar()è asincrona:

await Assert.ThrowsExceptionAsync<Exception>(() => foo.bar());

Se foo.bar()non è asincrono

Assert.ThrowsException<Exception>(() => foo.bar());

1
Ci sono molte altre risposte, per me stavo cercando un modo abbreviato per testare le condizioni di guasto conosciute solo dal tipo di eccezione, questo rende i casi di test più facili da leggere. NOTA: il Tipo di eccezione non corrisponde alle classi di eccezioni ereditate come un try-catch standard, quindi l'esempio sopra non intrappolerà un ArgumentExceptionesempio. Il vecchio Try Catch e test della risposta alle eccezioni è ancora preferito se hai criteri avanzati da testare, ma per molti dei miei casi, questo aiuta molto!
Chris Schaller,

5

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). Utilizzare un metodo di asserzione ben progettato, fornito dal framework di test o scrivere il proprio. Ecco cosa ho scritto e usato.

public static class ExceptionAssert
{
    private static T GetException<T>(Action action, string message="") where T : Exception
    {
        try
        {
            action();
        }
        catch (T exception)
        {
            return exception;
        }
        throw new AssertFailedException("Expected exception " + typeof(T).FullName + ", but none was propagated.  " + message);
    }

    public static void Propagates<T>(Action action) where T : Exception
    {
        Propagates<T>(action, "");
    }

    public static void Propagates<T>(Action action, string message) where T : Exception
    {
        GetException<T>(action, message);
    }

    public static void Propagates<T>(Action action, Action<T> validation) where T : Exception
    {
        Propagates(action, validation, "");
    }

    public static void Propagates<T>(Action action, Action<T> validation, string message) where T : Exception
    {
        validation(GetException<T>(action, message));
    }
}

L'esempio usa:

    [TestMethod]
    public void Run_PropagatesWin32Exception_ForInvalidExeFile()
    {
        (test setup that might propagate Win32Exception)
        ExceptionAssert.Propagates<Win32Exception>(
            () => CommandExecutionUtil.Run(Assembly.GetExecutingAssembly().Location, new string[0]));
        (more asserts or something)
    }

    [TestMethod]
    public void Run_PropagatesFileNotFoundException_ForExecutableNotFound()
    {
        (test setup that might propagate FileNotFoundException)
        ExceptionAssert.Propagates<FileNotFoundException>(
            () => CommandExecutionUtil.Run("NotThere.exe", new string[0]),
            e => StringAssert.Contains(e.Message, "NotThere.exe"));
        (more asserts or something)
    }

APPUNTI

Restituire l'eccezione invece di supportare un callback di convalida è un'idea ragionevole, tranne per il fatto che la sintassi della chiamata di questa asserzione è molto diversa rispetto alle altre asserzioni che uso.

A differenza di altri, utilizzo "propagati" anziché "tiri" poiché possiamo solo verificare se un'eccezione si propaga da una chiamata. Non possiamo testare direttamente che viene generata un'eccezione. Ma suppongo che tu possa immaginare dei tiri per dire: lanciati e non catturati.

PENSIERO FINALE

Prima di passare a questo tipo di approccio ho considerato l'utilizzo dell'attributo ExpectedException quando un test ha verificato solo il tipo di eccezione e l'utilizzo di un blocco try / catch se è stata richiesta una maggiore convalida. Ma non solo dovrei pensare a quale tecnica usare per ogni test, ma cambiare il codice da una tecnica all'altra in base alle esigenze cambiate non è stato uno sforzo banale. L'uso di un approccio coerente consente di risparmiare sforzo mentale.

Quindi, in sintesi, questo approccio mette in mostra: facilità d'uso, flessibilità e robustezza (difficile da sbagliare).


4

L'helper fornito da @Richiban sopra funziona alla grande tranne che non gestisce la situazione in cui viene generata un'eccezione, ma non il tipo previsto. I seguenti indirizzi che:

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace YourProject.Tests
{
    public static class MyAssert
    {
        /// <summary>
        /// Helper for Asserting that a function throws an exception of a particular type.
        /// </summary>
        public static void Throws<T>( Action func ) where T : Exception
        {
            Exception exceptionOther = null;
            var exceptionThrown = false;
            try
            {
                func.Invoke();
            }
            catch ( T )
            {
                exceptionThrown = true;
            }
            catch (Exception e) {
                exceptionOther = e;
            }

            if ( !exceptionThrown )
            {
                if (exceptionOther != null) {
                    throw new AssertFailedException(
                        String.Format("An exception of type {0} was expected, but not thrown. Instead, an exception of type {1} was thrown.", typeof(T), exceptionOther.GetType()),
                        exceptionOther
                        );
                }

                throw new AssertFailedException(
                    String.Format("An exception of type {0} was expected, but no exception was thrown.", typeof(T))
                    );
            }
        }
    }
}

2
Hmmm ... Capisco l'idea, ma non sono sicuro di essere d'accordo che sia meglio. Solo perché vogliamo garantire che venga sollevata un'eccezione specifica, ciò non significa che tutti gli altri dovrebbero essere racchiusi in un fallimento dell'asserzione. IMHO, un'eccezione sconosciuta dovrebbe semplicemente far saltare lo stack come in qualsiasi altra operazione di asserzione.
Crono,

@Martin Rimuoverei il codice relativo all'eccezione Altro e semplicemente riprogrammerei dalla seconda clausola di cattura
Tom Lint,

4

Dato che accenni all'uso di altre classi di test, un'opzione migliore ExpectedExceptiondell'attributo è quella di utilizzare Shoudly 's Should.Throw .

Should.Throw<DivideByZeroException>(() => { MyDivideMethod(1, 0); });

Diciamo che abbiamo l'obbligo che il cliente abbia un indirizzo per creare un ordine . In caso contrario, il CreateOrderForCustomermetodo dovrebbe risultare in un ArgumentException. Quindi potremmo scrivere:

[TestMethod]
public void NullUserIdInConstructor()
{
  var customer = new Customer(name := "Justin", address := null};

  Should.Throw<ArgumentException>(() => {
    var order = CreateOrderForCustomer(customer) });
}

Questo è meglio che usare un ExpectedException attributo perché siamo precisi su cosa dovrebbe generare l'errore. Ciò rende più chiari i requisiti nei nostri test e semplifica anche la diagnosi quando il test fallisce.

Si noti che esiste anche un Should.ThrowAsynctest del metodo asincrono.


4

In alternativa, puoi provare a testare le eccezioni vengono effettivamente lanciate con le 2 righe successive nel test.

var testDelegate = () => MyService.Method(params);
Assert.Throws<Exception>(testDelegate);

4

Nel VS unit test integrato se si desidera semplicemente verificare che venga generata "qualsiasi eccezione", ma non si conosce il tipo, è possibile utilizzare un catch all:

[TestMethod]
[ExpectedException(typeof(Exception), AllowDerivedTypes = true)]
public void ThrowExceptionTest()
{
    //...
}

3

Bene, riassumerò praticamente quello che tutti gli altri qui hanno detto prima ... Comunque, ecco il codice che ho costruito secondo le buone risposte :) Tutto ciò che resta da fare è copiare e usare ...

/// <summary>
/// Checks to make sure that the input delegate throws a exception of type TException.
/// </summary>
/// <typeparam name="TException">The type of exception expected.</typeparam>
/// <param name="methodToExecute">The method to execute to generate the exception.</param>
public static void AssertRaises<TException>(Action methodToExecute) where TException : System.Exception
{
    try
    {
        methodToExecute();
    }
    catch (TException) {
        return;
    }  
    catch (System.Exception ex)
    {
        Assert.Fail("Expected exception of type " + typeof(TException) + " but type of " + ex.GetType() + " was thrown instead.");
    }
    Assert.Fail("Expected exception of type " + typeof(TException) + " but no exception was thrown.");  
}


2

Questo dipenderà da quale framework di test stai usando?

In MbUnit, ad esempio, puoi specificare l'eccezione prevista con un attributo per assicurarti di ottenere l'eccezione che davvero ti aspetti.

[ExpectedException(typeof(ArgumentException))]

2

In caso di utilizzo di NUnit , prova questo:

Assert.That(() =>
        {
            Your_Method_To_Test();
        }, Throws.TypeOf<Your_Specific_Exception>().With.Message.EqualTo("Your_Specific_Message"));

2

C'è una fantastica libreria chiamata NFluent che accelera e facilita il modo in cui scrivi le tue affermazioni .

È abbastanza semplice scrivere un'asserzione per lanciare un'eccezione:

    [Test]
    public void given_when_then()
    {
        Check.ThatCode(() => MethodToTest())
            .Throws<Exception>()
            .WithMessage("Process has been failed");
    }

1

Anche se questa è una vecchia domanda, vorrei aggiungere un nuovo pensiero alla discussione. Ho esteso il modello Arrange, Act, Assert per essere previsto, Arrange, Act, Assert. È possibile creare un puntatore eccezione previsto, quindi affermare che è stato assegnato. Questo sembra più pulito che eseguire i tuoi Assert in un blocco catch, lasciando la sezione Act principalmente solo per una riga di codice per chiamare il metodo sotto test. Inoltre non è necessario Assert.Fail();o returnda più punti nel codice. Qualsiasi altra eccezione generata provocherà il fallimento del test, perché non verrà rilevata e se viene generata un'eccezione del tipo previsto, ma non era quella che ti aspettavi, affermando contro il messaggio o altre proprietà di l'eccezione aiuta a garantire che il test non passi inavvertitamente.

[TestMethod]
public void Bar_InvalidDependency_ThrowsInvalidOperationException()
{
    // Expectations
    InvalidOperationException expectedException = null;
    string expectedExceptionMessage = "Bar did something invalid.";

    // Arrange
    IDependency dependency = DependencyMocks.Create();
    Foo foo = new Foo(dependency);

    // Act
    try
    {
        foo.Bar();
    }
    catch (InvalidOperationException ex)
    {
        expectedException = ex;
    }

    // Assert
    Assert.IsNotNull(expectedException);
    Assert.AreEqual(expectedExceptionMessage, expectedException.Message);
}
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.