Najlepszy sposób na przetestowanie wyjątków za pomocą funkcji Assert, aby upewnić się, że zostaną wyrzucone

97

Czy uważasz, że to dobry sposób na testowanie wyjątków? Jakieś sugestie?

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

Assert.IsNotNull(exception);

Używam testu MS.

Hannoun Yassir
źródło

Odpowiedzi:

137

Używam kilku różnych wzorów. Używam ExpectedExceptionatrybutu przez większość czasu, gdy oczekiwany jest wyjątek. To wystarcza w większości przypadków, jednak w niektórych przypadkach jest to niewystarczające. Wyjątek może nie być możliwy do złapania - ponieważ jest wyrzucany przez metodę, która jest wywoływana przez odbicie - lub może po prostu chcę sprawdzić, czy inne warunki są utrzymane, powiedzmy, że transakcja została wycofana lub jakaś wartość jest nadal ustawiona. W takich przypadkach zawijam go w try/catchblok, który oczekuje dokładnego wyjątku, wykonuje polecenie, Assert.Failjeśli kod się powiedzie, a także przechwytuje ogólne wyjątki, aby upewnić się, że nie zostanie zgłoszony inny wyjątek.

Pierwszy przypadek:

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

Drugi przypadek:

[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
źródło
16
Wiele platform testowania jednostkowego implementuje błędy asercji jako wyjątki. Więc Assert.Fail () w drugim przypadku zostanie przechwycony przez blok catch (Exception), który ukryje komunikat o wyjątku. Musisz dodać haczyk (NUnit.Framework.AssertionException) {throw;} lub podobny - zobacz moją odpowiedź.
GrahamS,
@Graham - wpisałem to z czubka głowy. Zwykle wydrukowałbym również komunikat o wyjątku oprócz jego typu. Chodzi o to, że test zakończyłby się niepowodzeniem, ponieważ drugi program obsługi przechwyciłby niepowodzenie asercji i „odesłałby” informację o błędzie.
tvanfosson
1
Chociaż Twój kod jest funkcjonalnie sprawny, nie polecam używania atrybutu ExpectedException (ponieważ jest on zbyt ograniczający i podatny na błędy) lub pisać blok try / catch w każdym teście (ponieważ jest zbyt skomplikowany i podatny na błędy). Użyj dobrze zaprojektowanej metody asercji - dostarczonej przez platformę testową lub napisz własną. Możesz osiągnąć lepszy kod i nie będziesz musiał wybierać między różnymi technikami ani zmieniać jednej na drugą, gdy test się zmienia. Zobacz stackoverflow.com/a/25084462/2166177
steve
FYI - przeniosłem się do używania xUnit, który ma silnie wpisaną Assert.Throwsmetodę, która obejmuje oba te przypadki.
tvanfosson
Atrybut ExpectedException jest nieprzyjemny i przestarzały sposób testowania, czy wyjątki są generowane. Zobacz moją pełną odpowiedź poniżej.
bytedev
45

Teraz, 2017, możesz to zrobić łatwiej dzięki nowej platformie MSTest V2 Framework :

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

//async version
await Assert.ThrowsExceptionAsync<SomeException>(
  () => myObject.SomeMethodAsync()
);
Icaro Bombonato
źródło
To się powiedzie tylko wtedy, gdy System.Exceptionzostanie wyrzucony. Każdy inny, na przykład System.ArgumentException, nie zda testu.
sschoof
2
Jeśli spodziewasz się innego typu wyjątku, powinieneś przetestować go ... W swoim przykładzie powinieneś zrobić: Assert.ThrowsException <ArgumentException> (() => myClass.MyMethodWithError ());
Icaro Bombonato,
2
Należy zwrócić uwagę na to, że użycie Assert.ThrowsException<MyException>will testuje wyłącznie podany typ wyjątku, a nie żaden z jego pochodnych typów wyjątków. W naszym przykładzie, jeśli badana Subbyła Throww MyInheritedException(typu pochodnych z klasy podstawowego MyException), po czym badanie to nie .
Ama
Jeśli chcesz rozszerzyć test i zaakceptować typ wyjątku, a także jego typy pochodne, użyj rozszerzenia Try { SubToTest(); Assert.Fail("...") } Catch (AssertFailedException e) {throw;} Catch (MyException e) {...}. Zwróć uwagę na najważniejsze znaczenie Catch (AssertFailedException e) {throw;}(por. Komentarz od allgeek)
Ama,
16

Jestem tu nowy i nie mam reputacji, by komentować lub głosować przeciw, ale chciałem zwrócić uwagę na błąd w przykładzie w odpowiedzi Andy'ego White'a :

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

We wszystkich znanych mi platformach testowania jednostkowego Assert.Faildziała poprzez zgłaszanie wyjątku, więc ogólny catch faktycznie maskuje niepowodzenie testu. Jeśli SomethingThatCausesAnException()nie rzuca, Assert.Failwola, ale to nigdy nie wypłynie do biegacza testowego, aby wskazać niepowodzenie.

Jeśli chcesz przechwycić oczekiwany wyjątek (tj. Potwierdzić pewne szczegóły, takie jak komunikat / właściwości wyjątku), ważne jest, aby przechwycić określony oczekiwany typ, a nie podstawową klasę wyjątku. Pozwoliłoby to Assert.Failna wypłynięcie wyjątku (zakładając, że nie wyrzucasz tego samego typu wyjątku, co struktura testów jednostkowych), ale nadal zezwala na walidację wyjątku, który został zgłoszony przez Twoją SomethingThatCausesAnException()metodę.

allgeek
źródło
15

Od wersji 2.5 NUnit ma następujące poziomy metod Asserttestowania wyjątków:

Assert.Throws , który przetestuje dokładny typ wyjątku:

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

I Assert.Catch, który będzie testować pod kątem wyjątku danego typu lub typu wyjątku pochodzącego z tego typu:

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

Tak na marginesie, podczas debugowania testów jednostkowych, które generują wyjątki, może chcesz zapobiec VS od zerwania na wyjątek .

Edytować

Żeby podać przykład komentarza Mateusza poniżej, powrót ogólnego Assert.Throwsi Assert.Catchjest wyjątkiem z rodzajem wyjątku, który możesz następnie zbadać do dalszej analizy:

// The type of ex is that of the generic type parameter (SqlException)
var ex = Assert.Throws<SqlException>(() => MethodWhichDeadlocks());
Assert.AreEqual(1205, ex.Number);
StuartLC
źródło
2
Roy Osherove zaleca to w The Art of Unit Testing, drugie wydanie, sekcja 2.6.2.
Avi,
2
Podoba mi się Assert.Throws, że dodatkowo zwraca wyjątek, więc możesz pisać dalsze potwierdzenia na samym wyjątku.
Matthew
Pytanie dotyczyło MSTest, a nie NUnit.
bytedev
Pierwotne pytanie @nashwan OP nie miało takiej kwalifikacji, a tagowanie nadal nie kwalifikuje MS-Test. W obecnej postaci jest to pytanie dotyczące testów jednostkowych C #, .Net.
StuartLC
11

Niestety MSTest STILL tak naprawdę ma tylko atrybut ExpectedException (pokazuje, jak bardzo MS dba o MSTest), który IMO jest dość okropny, ponieważ łamie wzorzec Arrange / Act / Assert i nie pozwala dokładnie określić, której linii kodu oczekujesz wyjątku występować na.

Kiedy używam (/ zmuszony przez klienta) do używania MSTest, zawsze używam tej klasy pomocniczej:

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

Przykład użycia:

AssertException.Throws<ArgumentNullException>(() => classUnderTest.GetCustomer(null));
bytedev
źródło
10

Zamiast używać ExpectedExceptionatrybutu, czasami definiuję dwie pomocne metody dla moich klas testowych:

AssertThrowsException() przyjmuje delegata i potwierdza, że ​​zgłasza oczekiwany wyjątek z oczekiwanym komunikatem.

AssertDoesNotThrowException() przyjmuje tego samego delegata i zapewnia, że ​​nie zgłasza wyjątku.

To parowanie może być bardzo przydatne, gdy chcesz sprawdzić, czy wyjątek jest zgłaszany w jednym przypadku, ale nie w drugim.

Używając ich, mój kod testu jednostkowego może wyglądać następująco:

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

Ładnie i schludnie, co?

My AssertThrowsException()i AssertDoesNotThrowException()metody są zdefiniowane we wspólnej klasie bazowej w następujący sposób:

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
źródło
4

W większości frameworków do testów jednostkowych .net można umieścić atrybut [ExpectedException] w metodzie testowej. Jednak nie może to powiedzieć, że wyjątek wystąpił w momencie, w którym się tego spodziewałeś. To miejsce, gdzie xunit.net może pomóc.

Dzięki xunit masz Assert.Throws, więc możesz robić takie rzeczy:

    [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] to xunit odpowiednik [TestMethod]

Steve Willcock
źródło
Jeśli musisz używać MSTest (do czego często zmuszają mnie pracodawcy), zobacz moją odpowiedź poniżej.
bytedev,
4

Oznacz test ExpectedExceptionAttribute (jest to termin w NUnit lub MSTest; użytkownicy innych frameworków do testów jednostkowych mogą wymagać tłumaczenia).

itowlson
źródło
Nie używaj ExpectedExceptionAttribute (powód podany w moim poście poniżej). NUnit ma Assert.Throws <YourException> () i dla MSTest użyj czegoś podobnego do mojej klasy AssertException poniżej.
bytedev
0

Zaproponuj użycie czystej składni delegata NUnit .

Przykład testowania 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
źródło