Der beste Weg, um Ausnahmen mit Assert zu testen, um sicherzustellen, dass sie ausgelöst werden

97

Denken Sie, dass dies ein guter Weg ist, um Ausnahmen zu testen? Irgendwelche Vorschläge?

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

Assert.IsNotNull(exception);

Ich benutze MS Test.

Hannoun Yassir
quelle

Antworten:

137

Ich habe ein paar verschiedene Muster, die ich benutze. Ich benutze das ExpectedExceptionAttribut die meiste Zeit, wenn eine Ausnahme erwartet wird. Dies reicht in den meisten Fällen aus, es gibt jedoch einige Fälle, in denen dies nicht ausreicht. Die Ausnahme ist möglicherweise nicht abfangbar - da sie von einer Methode ausgelöst wird, die durch Reflektion aufgerufen wird - oder ich möchte nur überprüfen, ob andere Bedingungen gelten, z. B. wenn eine Transaktion zurückgesetzt wird oder noch ein Wert festgelegt wurde. In diesen Fällen verpacke ich es in einen try/catchBlock, der die genaue Ausnahme erwartet, eine, Assert.Failwenn der Code erfolgreich ist, und generische Ausnahmen abfängt, um sicherzustellen, dass keine andere Ausnahme ausgelöst wird.

Erster Fall:

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

Zweiter Fall:

[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
quelle
16
Viele Unit-Test-Frameworks implementieren Assertionsfehler als Ausnahmen. Daher wird Assert.Fail () im zweiten Fall vom catch-Block (Exception) abgefangen, wodurch die Ausnahmemeldung ausgeblendet wird. Sie müssen einen Fang (NUnit.Framework.AssertionException) {throw;} oder ähnliches hinzufügen - siehe meine Antwort.
GrahamS
@ Abraham - Ich habe das oben auf meinem Kopf getippt. Normalerweise würde ich zusätzlich zu ihrem Typ auch die Ausnahmemeldung ausdrucken. Der Punkt ist, dass der Test fehlschlagen würde, da der zweite Handler den Assertionsfehler abfangen und mit Informationen über den Fehler "zurückschlagen" würde.
Tvanfosson
1
Obwohl Ihr Code funktional einwandfrei ist, empfehle ich nicht, das ExpectedException-Attribut zu verwenden (da es zu einschränkend und fehleranfällig ist) oder in jedem Test einen Try / Catch-Block zu schreiben (da es zu kompliziert und fehleranfällig ist). Verwenden Sie eine gut konzipierte Assert-Methode - entweder von Ihrem Test-Framework bereitgestellt oder schreiben Sie Ihre eigene. Sie können besseren Code erzielen und müssen nicht zwischen den verschiedenen Techniken wählen oder von einer zur anderen wechseln, wenn sich der Test ändert. Siehe stackoverflow.com/a/25084462/2166177
steve
Zu Ihrer Information: Ich habe mich für xUnit entschieden, das eine stark typisierte Assert.ThrowsMethode hat, die beide Fälle abdeckt.
Tvanfosson
Das ExpectedException-Attribut ist eine unangenehme und veraltete Methode, um zu testen, ob Ausnahmen ausgelöst werden. Siehe meine vollständige Antwort unten.
Bytedev
41

Jetzt, 2017, können Sie es mit dem neuen MSTest V2 Framework einfacher machen :

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

//async version
await Assert.ThrowsExceptionAsync<SomeException>(
  () => myObject.SomeMethodAsync()
);
Icaro Bombonato
quelle
Dies wird nur erfolgreich sein, wenn ein System.Exceptiongeworfen wird. Jeder andere, wie ein, System.ArgumentExceptionwird den Test nicht bestehen.
Sschoof
2
Wenn Sie einen anderen Ausnahmetyp erwarten, sollten Sie ihn testen ... In Ihrem Beispiel sollten Sie Folgendes tun: Assert.ThrowsException <ArgumentException> (() => myClass.MyMethodWithError ());
Icaro Bombonato
2
Es ist wichtig zu beachten, dass bei der Verwendung von Assert.ThrowsException<MyException>nur der angegebene Ausnahmetyp und nicht einer der abgeleiteten Ausnahmetypen getestet wird. In meinem Beispiel würde der Test fehlschlagen , wenn der Test Subauf Throweinen MyInheritedException(von der Basisklasse abgeleiteten Typ MyException) wäre .
Ama
Wenn Sie Ihren Test erweitern und einen Ausnahmetyp sowie dessen abgeleitete Typen akzeptieren möchten, verwenden Sie a Try { SubToTest(); Assert.Fail("...") } Catch (AssertFailedException e) {throw;} Catch (MyException e) {...}. Beachten Sie die höchste Bedeutung von Catch (AssertFailedException e) {throw;}(vgl. Kommentar von allgeek)
Ama
17

Ich bin neu hier und habe nicht den Ruf, Kommentare abzugeben oder abzustimmen, wollte aber auf einen Fehler im Beispiel in Andy Whites Antwort hinweisen :

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

In allen mir bekannten Unit-Testing-Frameworks Assert.Failwird eine Ausnahme ausgelöst, sodass der generische Fang den Fehler des Tests tatsächlich maskiert. Wenn SomethingThatCausesAnException()nicht geworfen Assert.Failwird, sprudelt der Wille, aber das wird niemals zum Testläufer heraus, um einen Fehler anzuzeigen.

Wenn Sie die erwartete Ausnahme abfangen müssen (dh um bestimmte Details wie die Nachricht / Eigenschaften der Ausnahme zu bestätigen), ist es wichtig, den spezifischen erwarteten Typ und nicht die Basisausnahmeklasse abzufangen. Dies würde es ermöglichen, dass die Assert.FailAusnahme heraussprudelt (vorausgesetzt, Sie lösen nicht die gleiche Art von Ausnahme aus, die Ihr Unit-Testing-Framework ausführt), ermöglicht jedoch weiterhin die Validierung der Ausnahme, die von Ihrer SomethingThatCausesAnException()Methode ausgelöst wurde .

Allgeek
quelle
15

Ab Version 2.5 verfügt NUnit über die folgenden Methodenebenen Assertzum Testen von Ausnahmen:

Assert.Throws , das auf einen genauen Ausnahmetyp prüft :

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

Und Assert.Catch, die auf eine Ausnahme eines bestimmten Typs oder einen von diesem Typ abgeleiteten Ausnahmetyp testen:

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

Abgesehen davon möchten Sie beim Debuggen von Komponententests, die Ausnahmen auslösen, möglicherweise verhindern, dass VS die Ausnahme beschädigt .

Bearbeiten

Nur um ein Beispiel für Matthews Kommentar unten zu geben, die Rückgabe des Generikums Assert.Throwsund Assert.Catchist die Ausnahme mit dem Typ der Ausnahme, die Sie dann zur weiteren Prüfung untersuchen können:

// The type of ex is that of the generic type parameter (SqlException)
var ex = Assert.Throws<SqlException>(() => MethodWhichDeadlocks());
Assert.AreEqual(1205, ex.Number);
StuartLC
quelle
2
Roy Osherove empfiehlt dies in The Art of Unit Testing, 2. Auflage, Abschnitt 2.6.2.
Avi
2
Ich mag Assert.Throws, außerdem gibt es die Ausnahme zurück, so dass Sie weitere Aussagen über die Ausnahme selbst schreiben können.
Matthew
Die Frage war für MSTest nicht NUnit.
Bytedev
Die ursprüngliche Frage von @nashwan OP hatte diese Qualifikation nicht und das Tagging qualifiziert den MS-Test immer noch nicht. Derzeit handelt es sich um eine C # -, .Net- oder Unit-Testing-Frage.
StuartLC
11

Leider hat MSTest STILL nur wirklich das ExpectedException-Attribut (zeigt nur, wie sehr sich MS um MSTest kümmert), was IMO ziemlich schrecklich ist, weil es das Arrange / Act / Assert-Muster bricht und es Ihnen nicht erlaubt, genau anzugeben, welche Codezeile Sie für die Ausnahme erwarten auftreten am.

Wenn ich MSTest verwende (/ von einem Client gezwungen), verwende ich immer diese Hilfsklasse:

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

Anwendungsbeispiel:

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

Als Alternative zur Verwendung von ExpectedExceptionAttributen definiere ich manchmal zwei hilfreiche Methoden für meine Testklassen:

AssertThrowsException() Nimmt einen Delegaten und behauptet, dass er die erwartete Ausnahme mit der erwarteten Nachricht auslöst.

AssertDoesNotThrowException() Nimmt denselben Delegaten und behauptet, dass er keine Ausnahme auslöst.

Diese Kopplung kann sehr nützlich sein, wenn Sie testen möchten, ob in einem Fall eine Ausnahme ausgelöst wird, im anderen jedoch nicht.

Mit ihnen könnte mein Unit-Test-Code folgendermaßen aussehen:

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

Schön und ordentlich, oder?

Meine AssertThrowsException()und AssertDoesNotThrowException()Methoden werden in einer gemeinsamen Basisklasse wie folgt definiert:

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
quelle
4

Markieren Sie den Test mit dem ExpectedExceptionAttribute (dies ist der Begriff in NUnit oder MSTest; Benutzer anderer Unit-Test-Frameworks müssen möglicherweise übersetzen).

itowlson
quelle
Verwenden Sie nicht ExpectedExceptionAttribute (Grund in meinem Beitrag unten angegeben). NUnit hat Assert.Throws <YourException> () und verwendet für MSTest so etwas wie meine AssertException-Klasse unten.
Bytedev
3

Bei den meisten .net-Unit-Test-Frameworks können Sie der Testmethode ein Attribut [ExpectedException] hinzufügen. Dies kann Ihnen jedoch nicht sagen, dass die Ausnahme an dem Punkt aufgetreten ist, den Sie erwartet haben. Das ist , wo xunit.net helfen kann.

Mit xunit haben Sie Assert.Throws, damit Sie folgende Dinge tun können:

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

[Fakt] ist das Xunit-Äquivalent von [TestMethod]

Steve Willcock
quelle
Wenn Sie MSTest verwenden müssen (zu dem ich häufig von Arbeitgebern gezwungen werde), lesen Sie meine Antwort unten.
Bytedev
0

Schlagen Sie vor, die saubere Delegatensyntax von NUnit zu verwenden .

Beispiel zum Testen 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
quelle