Jak użyć Assert, aby zweryfikować zgłoszenie wyjątku?

830

Jak użyć Assert(lub innej klasy Test?), Aby sprawdzić, czy został zgłoszony wyjątek?

Alex
źródło
Z jakich ram testów jednostkowych korzystasz?
Kevin Pullin
3
Zintegrowane Visual Studio
Alex
4
Czy atrybut ExpectedException nie pomaga? ref: msdn.microsoft.com/en-us/library/…
shahkalpesh
2
Zabawne, właśnie skończyłem szukać odpowiedzi na to, znalazłem ją na stackoverflow.com/questions/741029/testing-exceptions .
dfjacobs

Odpowiedzi:

978

W przypadku „Visual Studio Team Test” wydaje się, że stosuje się atrybut ExpectedException do metody testu.

Przykład z dokumentacji tutaj: Przewodnik po testach jednostkowych z 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");
}
Kevin Pullin
źródło
25
Powyższy atrybut ExpectedException działa również w NUnit (ale [TestMethod] powinien być [Test]).
dbkk
5
@dbkk: Nie działa dokładnie tak samo w NUnit - wiadomość jest traktowana jako ciąg znaków, który musi spełnić komunikat wyjątku (i IU uważa, że ​​to ma więcej sensu)
Ruben Bartelink
29
Ten atrybut wykonuje zadanie i jest wbudowaną funkcją dla programistów c #, ale nie polecam go używać, ponieważ nie jest wystarczająco elastyczny. Zastanów się, co się stanie, jeśli typ wyjątku zostanie zgłoszony przez kod konfiguracji testu: test przeszedł pomyślnie, ale tak naprawdę nie zrobił tego, czego się spodziewałeś. A co jeśli chcesz przetestować stan obiektu wyjątku. Zwykle chcę używać StringAssert.Contains (e.Message ...) zamiast testować całą wiadomość. Użyj metody aser, jak opisano w innych odpowiedziach.
steve
3
Unikaj używania ExpectedException w NUnit, ponieważ zostanie on usunięty w NUnit 3.0. Wolę używać Assert.Throws <SpecificException> ()
Terence
5
Możesz użyć Assert.ThrowsException <T> i Assert.ThrowsExceptionAsync <T> w ramach MsTest.
Gopal Krishnan
257

Zazwyczaj Twoja platforma testowania będzie na to odpowiedź. Ale jeśli nie jest wystarczająco elastyczny, zawsze możesz to zrobić:

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

Jak zauważa @Jonas, NIE DZIAŁA to w przypadku przechwycenia podstawowego wyjątku:

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

Jeśli absolutnie musisz złapać wyjątek, musisz ponownie uruchomić Assert.Fail (). Ale tak naprawdę jest to znak, że nie powinieneś pisać tego ręcznie; sprawdź środowisko testowe pod kątem opcji lub sprawdź, czy możesz zgłosić bardziej znaczący wyjątek do przetestowania.

catch (AssertionException) { throw; }

Powinieneś być w stanie dostosować to podejście do tego, co lubisz - w tym określając, jakie wyjątki należy złapać. Jeśli oczekujesz tylko niektórych typów, zakończ catchbloki:

} catch (GoodException) {
} catch (Exception) {
    // not the right kind of exception
    Assert.Fail();
}
ojrac
źródło
20
+1, używam tej metody zamiast atrybutu, gdy muszę dokonywać stwierdzeń wykraczających poza typ wyjątku. Na przykład co zrobić, jeśli trzeba sprawdzić, czy pewne pola w instancji wyjątku mają ustawione określone wartości.
Pavel Repin
2
Nie musisz określać komunikatu o błędzie. To wystarczy: [oczekiwany
wyjątek
5
Myślę, że to rozwiązanie jest najlepsze. [Oczekiwany wyjątek (typeof (ArgumentException))] ma swoje zastosowania, jeśli test jest prosty, ale moim zdaniem jest to leniwe rozwiązanie i wygoda może prowadzić do pułapek. To rozwiązanie daje ci konkretną kontrolę, aby wykonać bardziej poprawny test, a ponadto możesz przetestować Writeline do raportu z przebiegu testu, że wyjątek rzeczywiście został zgłoszony zgodnie z oczekiwaniami.
evilfish
12
Uważaj na to, ponieważ Assert.Fail () zgłasza wyjątek, jeśli go złapiesz, test kończy się pomyślnie!
Jonas
4
@ Vinnyq12 Chodzi mi o to, że pierwszy test w powyższym przykładzie nigdy się nie powiedzie. Test kończy się niepowodzeniem, jeśli zostanie zgłoszony wyjątek (a nie „catch” przez ExpectedExceptionAttribute)
Jonas
113

Moją preferowaną metodą realizacji tego jest napisanie metody o nazwie Rzuty i użycie jej tak jak każdej innej metody Assert. Niestety .NET nie pozwala na napisanie metody rozszerzenia statycznego, więc nie można użyć tej metody, jakby faktycznie należała do kompilacji w klasie Assert; po prostu stwórz inny o nazwie MyAssert lub coś podobnego. Klasa wygląda następująco:

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

Oznacza to, że test jednostkowy wygląda następująco:

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

Który wygląda i zachowuje się bardziej jak reszta składni testu jednostkowego.

Richiban
źródło
1
Pozbądź się flagi bool i rzuć linię bezpośrednio po wywołaniu, aby uzyskać bardziej kompaktową implementację.
gt
11
Jedyną rzeczą, która to poprawia, jest zwrócenie wychwyconego wyjątku przez funkcję, abyś mógł dalej twierdzić, że takie rzeczy jak atrybuty wyjątku są poprawne.
Mark Hildreth,
2
Dzięki! Wydaje mi się to najlepszym podejściem, ponieważ jest to krótki sposób na przetestowanie wielu wyjątków w jednej metodzie. Jest także o wiele bardziej czytelny.
David Sherret,
2
@MickeyPerlstein Atrybuty łamią reguły AAA dotyczące testowania. W szczególności, jeśli Twój aranżacja zdarzy się rzucić wyjątek, zanim jeszcze dojdziesz do aktu, wtedy test zdaje egzamin ... eek!
freedomn-m
2
Microsoft w końcu zajął się aktualizacją MSTest - obsługa v2 Assert.ThrowsException<T>i Assert.ThrowsExceptionAsync<T>- patrz blogs.msdn.microsoft.com/visualstudioalm/2017/02/25/…
Quango
62

jeśli używasz NUNIT, możesz zrobić coś takiego:

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


Możliwe jest również zapisanie zgłoszonego wyjątku w celu jego dalszego sprawdzenia:

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

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

damir
źródło
60

Jeśli używasz MSTest, który pierwotnie nie miał ExpectedExceptionatrybutu, możesz to zrobić:

try 
{
    SomeExceptionThrowingMethod()
    Assert.Fail("no exception thrown");
}
catch (Exception ex)
{
    Assert.IsTrue(ex is SpecificExceptionType);
}
Jon Limjap
źródło
3
To działa, ale ogólnie nie polecam tego, ponieważ logika jest zbyt skomplikowana. Nie mówię, że jest zawiłe, ale zastanów się, czy napiszesz ten blok kodu dla wielu testów - 10s, 100s testów. Tę logikę należy zastosować w dobrze zaprojektowanej metodzie potwierdzenia. Zobacz inne odpowiedzi.
steve
35

Uważaj na używanie ExpectedException, ponieważ może to prowadzić do kilku pułapek, jak pokazano tutaj:

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

I tu:

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

Jeśli chcesz przetestować wyjątki, sposoby są mniej niezadowolone. Możesz użyć metody try {act / fail} catch {assert}, która może być przydatna w środowiskach, które nie mają bezpośredniego wsparcia dla testów wyjątków innych niż ExpectedException.

Lepszą alternatywą jest użycie xUnit.NET, który jest bardzo nowoczesnym, przyszłościowym i rozszerzalnym środowiskiem testowania jednostek, który wyciągnął wnioski ze wszystkich innych błędów i został ulepszony. Jednym z takich ulepszeń jest Assert.Throws, który zapewnia znacznie lepszą składnię do zapewniania wyjątków.

Możesz znaleźć xUnit.NET na github: http://xunit.github.io/

jrista
źródło
4
Zauważ, że NUnit 2.5 obsługuje teraz także składnię stylu Assert.Throws
nunit.com/index.php?p=releaseNotes&r=2.5
Sposób, w jaki testy jednostkowe zatrzymują się, aby poinformować cię o wyjątku podczas korzystania z ExpectedException, doprowadza mnie do szału. Dlaczego stwardnienie rozsiane uważało, że dobrym pomysłem jest ręczny krok w automatycznych testach? Dzięki za linki.
Ant
@Ant: MS skopiowało NUnit ... więc prawdziwe pytanie brzmi, dlaczego NUnit uznał, że to dobry pomysł?
jrista
28

MSTest (v2) ma teraz funkcję Assert.ThrowsException, której można użyć w następujący sposób:

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

Możesz zainstalować go za pomocą nuget: Install-Package MSTest.TestFramework

Martin Beeby
źródło
W 2018 roku jest to uważane za najlepszą praktykę, ponieważ sprawdza, czy tylko testowana jednostka rzuca, a nie jakiś inny kod.
CM
24

W projekcie, nad którym pracuję, mamy inne rozwiązanie.

Po pierwsze, nie podoba mi się ExpectedExceptionAttribute, ponieważ bierze pod uwagę wywołanie metody, które spowodowało wyjątek.

Zamiast tego robię to za pomocą metody pomocy.

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

Zgrabnie, prawda?)

Glenn
źródło
14

Jest to atrybut metody testowej ... nie używasz Assert. Wygląda tak:

[ExpectedException(typeof(ExceptionType))]
public void YourMethod_should_throw_exception()
bytebender
źródło
13

Możesz pobrać pakiet z Nuget, używając: PM> Install-Package MSTestExtensions, który dodaje składnię Assert.Throws () w stylu nUnit / xUnit do MsTest.

Instrukcje wysokiego poziomu: pobierz zestaw i odziedzicz z BaseTest i możesz użyć składni Assert.Throws () .

Główna metoda implementacji rzutów wygląda następująco:

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

Ujawnienie: Złożyłem ten pakiet.

Więcej informacji: http://www.bradoncode.com/blog/2012/01/asserting-exceptions-in-mstest-with.html

Bradley Braithwaite
źródło
Dzięki za przykład. Czy masz przykład testowania Assert.DoesNotThrow () lub równoważnego?
Lane Goolsby
10

Możesz to osiągnąć za pomocą prostej linii.

Jeśli operacja foo.bar()jest asynchroniczna:

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

Jeśli foo.bar()nie jest asynchroniczne

Assert.ThrowsException<Exception>(() => foo.bar());
Cfrim
źródło
1
Istnieje wiele innych odpowiedzi, dla mnie szukałem skróconego sposobu testowania znanych stanów awaryjnych tylko według typu wyjątku, co zapewnia najłatwiejsze do odczytania przypadki testowe. UWAGA: typ wyjątku nie pasuje do dziedziczonych klas wyjątków, takich jak standardowy try-catch, więc powyższy przykład nie pułapkuje ArgumentExceptionna przykład. Stara Try Catch i testowanie odpowiedzi wyjątku jest nadal preferowana, jeśli masz zaawansowane kryteria testowania, ale w wielu moich przypadkach bardzo to pomaga!
Chris Schaller
5

Nie polecam używania atrybutu ExpectedException (ponieważ jest zbyt ograniczający i podatny na błędy) ani pisania bloku try / catch w każdym teście (ponieważ jest zbyt skomplikowany i podatny na błędy). Użyj dobrze zaprojektowanej metody asercji - dostarczonej przez środowisko testowe lub napisz własną. Oto, co napisałem i wykorzystałem.

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

Przykładowe zastosowania:

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

UWAGI

Zwrócenie wyjątku zamiast obsługi wywołania zwrotnego sprawdzania poprawności jest rozsądnym pomysłem, z wyjątkiem tego, że powoduje to, że składnia wywołania tego asertu jest bardzo różna od innych twierdzeń, których używam.

W przeciwieństwie do innych, używam „propaguje” zamiast „rzuca”, ponieważ możemy jedynie przetestować, czy wyjątek propaguje się z wywołania. Nie możemy bezpośrednio przetestować, czy zgłoszony został wyjątek. Ale przypuszczam, że wyobrażenie o rzutach może oznaczać: rzucone i nie złapane.

KOŃCOWA MYŚL

Przed przejściem do tego rodzaju podejścia rozważałem użycie atrybutu ExpectedException, gdy test zweryfikował tylko typ wyjątku i użycie bloku try / catch, jeśli wymagana była większa weryfikacja. Ale nie tylko musiałbym zastanowić się, którą technikę zastosować w każdym teście, ale także zmienić kod z jednej techniki na drugą, gdy zmieniły się potrzeby, nie było trywialnym wysiłkiem. Stosowanie jednego spójnego podejścia oszczędza wysiłek umysłowy.

Podsumowując, podejście to cechuje: łatwość użycia, elastyczność i solidność (trudno to zrobić źle).

Steve
źródło
4

Pomocnik udostępniony przez @Richiban powyżej działa świetnie, z tym że nie radzi sobie z sytuacją, w której zgłoszony jest wyjątek, ale nie jest oczekiwanym typem. Następujące adresy, które:

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))
                    );
            }
        }
    }
}
Martin Connell
źródło
2
Hmmm ... Rozumiem pomysł, ale nie jestem pewien, czy zgadzam się, że jest lepszy. To, że chcemy zapewnić zgłoszenie wyjątku, nie oznacza, że ​​wszystkie pozostałe powinny zostać opakowane jako niepowodzenie asercji. IMHO nieznany wyjątek powinien po prostu wygasić stos, tak jak w przypadku każdej innej operacji potwierdzenia.
Crono
@Martin Usunę kod dotyczący wyjątkuInne i po prostu wrócę z drugiej klauzuli catch
Tom Lint
4

Ponieważ wspominasz o użyciu innych klas testowych, lepszym rozwiązaniem niż ExpectedExceptionatrybut jest użycie Shoudly 's Should.Throw .

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

Powiedzmy, że mamy wymóg, aby klient musiał mieć adres, aby utworzyć zamówienie . Jeśli nie, CreateOrderForCustomermetoda powinna spowodować ArgumentException. Następnie moglibyśmy napisać:

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

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

Jest to lepsze niż użycie ExpectedExceptionatrybutu, ponieważ dokładniej określamy, co powinno spowodować błąd. To sprawia, że ​​wymagania w naszych testach są jaśniejsze, a także ułatwia diagnozę, gdy test się nie powiedzie.

Uwaga: istnieje również Should.ThrowAsyncmetoda asynchronicznego testowania.

Justin J Stark
źródło
4

Alternatywnie możesz spróbować przetestować wyjątki wyrzucane z następnymi 2 liniami w teście.

var testDelegate = () => MyService.Method(params);
Assert.Throws<Exception>(testDelegate);
Matias
źródło
4

W przypadku wbudowanego testowania jednostkowego VS, jeśli po prostu chcesz sprawdzić, czy zgłoszony został „dowolny wyjątek”, ale nie znasz typu, możesz użyć catch all:

[TestMethod]
[ExpectedException(typeof(Exception), AllowDerivedTypes = true)]
public void ThrowExceptionTest()
{
    //...
}
TTT
źródło
3

Cóż, podsumuję to, co wszyscy tu wcześniej mówili ... W każdym razie, oto kod, który zbudowałem zgodnie z dobrymi odpowiedziami :) Pozostaje tylko skopiować i użyć ...

/// <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.");  
}
roeiba
źródło
2

Sprawdź Dokumenty nUnit, aby uzyskać przykłady dotyczące:

[ExpectedException( typeof( ArgumentException ) )]
Jon Masters
źródło
2

Będzie to zależeć od używanej struktury testowej?

Na przykład w MbUnit możesz określić oczekiwany wyjątek za pomocą atrybutu, aby upewnić się, że otrzymujesz wyjątek, którego naprawdę oczekujesz.

[ExpectedException(typeof(ArgumentException))]
Sójka
źródło
2

W przypadku korzystania z NUnit , spróbuj tego:

Assert.That(() =>
        {
            Your_Method_To_Test();
        }, Throws.TypeOf<Your_Specific_Exception>().With.Message.EqualTo("Your_Specific_Message"));
Amir Chatrbahr
źródło
2

Istnieje niesamowita biblioteka o nazwie NFluent, która przyspiesza i ułatwia sposób pisania swoich twierdzeń .

Łatwo jest napisać twierdzenie o zgłoszeniu wyjątku:

    [Test]
    public void given_when_then()
    {
        Check.ThatCode(() => MethodToTest())
            .Throws<Exception>()
            .WithMessage("Process has been failed");
    }
mirind4
źródło
1

Mimo że jest to stare pytanie, chciałbym dodać do dyskusji nową myśl. Rozszerzyłem wzór Arrange, Act, Assert, którego można się spodziewać, Arrange, Act, Assert. Możesz zrobić oczekiwany wskaźnik wyjątku, a następnie potwierdzić, że został przypisany. Jest to czystsze niż wykonywanie Asertów w bloku catch, pozostawiając sekcję Act głównie tylko dla jednego wiersza kodu, który wywołuje testowaną metodę. Nie musisz także przechodzić do Assert.Fail();ani returnz wielu punktów w kodzie. Każdy inny zgłoszony wyjątek spowoduje, że test zakończy się niepowodzeniem, ponieważ nie zostanie przechwycony, a jeśli zostanie zgłoszony wyjątek typu oczekiwanego, ale nie był to oczekiwany, potwierdzający w stosunku do wiadomości lub innych właściwości wyjątek pomaga upewnić się, że test nie przejdzie przypadkowo.

[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);
}
Adam Venezia
źródło