Comment utiliser Assert pour vérifier qu'une exception a été levée?

830

Comment utiliser Assert(ou une autre classe de test?) Pour vérifier qu'une exception a été levée?

Alex
la source
Quel cadre de tests unitaires utilisez-vous?
Kevin Pullin
3
Visual Studio Integrated
Alex
4
L'attribut ExpectedException n'aide-t-il pas? ref: msdn.microsoft.com/en-us/library/…
shahkalpesh
2
Drôle, je viens de terminer la recherche de la réponse à cette question, je l'ai trouvée sur stackoverflow.com/questions/741029/testing-exceptions .
dfjacobs

Réponses:

978

Pour "Visual Studio Team Test", il apparaît que vous appliquez l'attribut ExpectedException à la méthode du test.

Exemple de documentation ici: Procédure pas à pas de test unitaire avec 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
la source
25
L'attribut ExpectedException ci-dessus fonctionne également dans NUnit (mais [TestMethod] devrait être [Test]).
dbkk
5
@dbkk: ne fonctionne pas exactement de la même manière dans NUnit - le message est traité comme une chaîne qui doit correspondre au message d'exception (et je pense que cela a plus de sens)
Ruben Bartelink
29
Cet attribut fait le travail et est une fonctionnalité intégrée pour les programmeurs c #, mais je ne recommande pas de l'utiliser car il n'est pas assez flexible. Considérez ce qui se passe si le type d'exception est levé par votre code de configuration de test: le test réussit, mais n'a pas fait ce que vous attendiez. Ou que faire si vous souhaitez tester l'état de l'objet d'exception. Je souhaite généralement utiliser StringAssert.Contains (e.Message ...) plutôt que de tester l'ensemble du message. Utilisez une méthode d'affirmation comme décrit dans d'autres réponses.
steve
3
Évitez d'utiliser ExpectedException dans NUnit, car il sera supprimé dans NUnit 3.0. Je préfère utiliser Assert.Throws <SpecificException> ()
Terence
5
Vous pouvez utiliser Assert.ThrowsException <T> et Assert.ThrowsExceptionAsync <T> dans MsTest.
Gopal Krishnan le
257

Habituellement, votre framework de test aura une réponse à cela. Mais si ce n'est pas assez flexible, vous pouvez toujours le faire:

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

Comme le souligne @Jonas, cela ne fonctionne PAS pour intercepter une exception de base:

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

Si vous devez absolument attraper Exception, vous devez relancer Assert.Fail (). Mais vraiment, c'est un signe que vous ne devriez pas écrire cela à la main; vérifiez votre framework de test pour les options, ou voyez si vous pouvez lever une exception plus significative à tester.

catch (AssertionException) { throw; }

Vous devriez être en mesure d'adapter cette approche à tout ce que vous voulez, notamment en spécifiant les types d'exceptions à intercepter. Si vous ne vous attendez qu'à certains types, terminez les catchblocs avec:

} catch (GoodException) {
} catch (Exception) {
    // not the right kind of exception
    Assert.Fail();
}
ojrac
la source
20
+1, j'utilise cette méthode à la place de l'attribut lorsque j'ai besoin de faire des affirmations au-delà du type d'exception. Par exemple, que faire si l'on doit vérifier que certains champs de l'instance d'exception sont définis sur certaines valeurs.
Pavel Repin
2
Vous n'êtes pas obligé de spécifier le message d'erreur. Cela suffit: [ExpectedException (typeof (ArgumentException))]
mibollma
5
Je pense que cette solution est la meilleure. [ExpectedException (typeof (ArgumentException))] a ses utilisations, si le test est simple, mais c'est à mon avis une solution paresseuse et être à l'aise avec peut conduire à des pièges. Cette solution vous donne un contrôle spécifique pour effectuer un test plus correct, et vous pouvez également, pour un test Writeline dans votre rapport d'exécution de test, que l'exception a bien été levée comme prévu.
evilfish
12
Soyez prudent avec cela car Assert.Fail () déclenche une exception, si vous l'attrapez, le test réussit!
Jonas
4
@ Vinnyq12 Ce que je veux dire, c'est que le premier test dans l'exemple ci-dessus n'échouera jamais. Un test échoue si une exception est levée (et non "catch" par le ExpectedExceptionAttribute)
Jonas
113

Ma méthode préférée pour implémenter cela est d'écrire une méthode appelée Throws et de l'utiliser comme n'importe quelle autre méthode Assert. Malheureusement, .NET ne vous permet pas d'écrire une méthode d'extension statique, vous ne pouvez donc pas utiliser cette méthode comme si elle appartenait réellement à la génération dans la classe Assert; faites simplement un autre appelé MyAssert ou quelque chose de similaire. La classe ressemble à ceci:

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

Cela signifie que votre test unitaire ressemble à ceci:

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

Qui ressemble et se comporte beaucoup plus comme le reste de vos syntaxes de test unitaire.

Richiban
la source
1
Débarrassez-vous du drapeau booléen et placez le lancer directement après l'invocation pour une implémentation plus compacte.
gt
11
La seule chose qui améliore cela est que la fonction retourne l'exception interceptée afin que vous puissiez continuer à affirmer que des choses comme les attributs de l'exception sont correctes.
Mark Hildreth
2
Merci! Cela me semble être la meilleure approche car c'est un moyen court de tester plusieurs exceptions dans une seule méthode. C'est aussi beaucoup plus lisible.
David Sherret
2
Les attributs @MickeyPerlstein enfreignent les règles AAA pour les tests. Plus précisément, si votre Arrangement arrive à lever l'exception avant même de passer à la Loi, votre test réussit ... eek!
freedomn-m
2
Microsoft a enfin réussi à mettre à jour MSTest - supports v2 Assert.ThrowsException<T>et Assert.ThrowsExceptionAsync<T>- voir blogs.msdn.microsoft.com/visualstudioalm/2017/02/25/…
Quango
62

si vous utilisez NUNIT, vous pouvez faire quelque chose comme ceci:

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


Il est également possible de stocker l'exception levée afin de la valider davantage:

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

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

damir
la source
60

Si vous utilisez MSTest, qui à l'origine n'avait pas d' ExpectedExceptionattribut, vous pouvez le faire:

try 
{
    SomeExceptionThrowingMethod()
    Assert.Fail("no exception thrown");
}
catch (Exception ex)
{
    Assert.IsTrue(ex is SpecificExceptionType);
}
Jon Limjap
la source
3
Cela fonctionne, mais je ne le recommande pas en général car la logique est trop compliquée. Je ne dis pas que c'est compliqué, mais pensez à écrire ce bloc de code pour plusieurs tests - 10s, 100s de tests. Cette logique doit être transformée en une méthode d'assertion bien conçue. Voir d'autres réponses.
steve
35

Méfiez-vous de l'utilisation d'ExpectedException, car cela peut entraîner plusieurs pièges, comme illustré ici:

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

Et ici:

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

Si vous avez besoin de tester les exceptions, il existe moins de moyens désapprouvés. Vous pouvez utiliser la méthode try {act / fail} catch {assert}, qui peut être utile pour les frameworks qui ne prennent pas directement en charge les tests d'exception autres que ExpectedException.

Une meilleure alternative consiste à utiliser xUnit.NET, qui est un cadre de test unitaire très moderne, prospectif et extensible qui a appris de toutes les autres erreurs et s'est amélioré. Une telle amélioration est Assert.Throws, qui fournit une bien meilleure syntaxe pour affirmer des exceptions.

Vous pouvez trouver xUnit.NET sur github: http://xunit.github.io/

jrista
la source
4
Notez que NUnit 2.5 prend également en charge la syntaxe de style Assert.Throws maintenant - nunit.com/index.php?p=releaseNotes&r=2.5
Alconja
La façon dont les tests unitaires s'arrêtent pour vous informer de l'exception lors de l'utilisation d'ExceptionException me rend fou. Pourquoi MS a-t-il pensé que c'était une bonne idée d'avoir une étape manuelle dans les tests automatisés? Merci pour les liens.
Ant
@Ant: MS a copié NUnit ... alors la vraie question est, pourquoi NUnit a-t-il pensé que c'était une bonne idée?
jrista
28

MSTest (v2) a maintenant une fonction Assert.ThrowsException qui peut être utilisée comme ceci:

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

Vous pouvez l'installer avec nuget: Install-Package MSTest.TestFramework

Martin Beeby
la source
En 2018, cela est considéré comme la meilleure pratique car il vérifie que seule l'unité en cours de test est en train de lancer et non un autre code.
CM
24

Dans un projet sur lequel je travaille, nous avons une autre solution pour cela.

Tout d'abord, je n'aime pas le ExpectedExceptionAttribute car il prend en considération l'appel de méthode qui a provoqué l'exception.

Je fais cela avec une méthode d'aide à la place.

Tester

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

Neat, n'est-ce pas;)

Glenn
la source
14

C'est un attribut de la méthode de test ... vous n'utilisez pas Assert. Ressemble à ça:

[ExpectedException(typeof(ExceptionType))]
public void YourMethod_should_throw_exception()
bytebender
la source
13

Vous pouvez télécharger un package à partir de Nuget à l'aide de: PM> Install-Package MSTestExtensions qui ajoute la syntaxe Assert.Throws () dans le style nUnit / xUnit à MsTest.

Instructions de haut niveau: téléchargez l'assembly et héritez de BaseTest et vous pouvez utiliser la syntaxe Assert.Throws () .

La méthode principale pour l'implémentation de Throws se présente comme suit:

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

Divulgation: J'ai mis en place ce package.

Plus d'informations: http://www.bradoncode.com/blog/2012/01/asserting-exceptions-in-mstest-with.html

Bradley Braithwaite
la source
Merci pour l'exemple. Avez-vous un exemple de test d'un Assert.DoesNotThrow () ou équivalent?
Lane Goolsby
10

Vous pouvez y parvenir avec une simple ligne.

Si votre opération foo.bar()est asynchrone:

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

Si foo.bar()n'est pas asynchrone

Assert.ThrowsException<Exception>(() => foo.bar());
Cfrim
la source
1
Il y a beaucoup d'autres réponses, pour moi, je cherchais un moyen abrégé de tester les conditions d'échec connues par type d'exception uniquement, ce qui en fait les cas de test les plus lisibles. REMARQUE: le type d'exception ne correspond pas aux classes d'exceptions héritées comme un try-catch standard, donc l'exemple ci-dessus ne piège pas un ArgumentExceptionpar exemple. L'ancien Try Catch et test de la réponse d'exception est toujours préféré si vous avez des critères avancés à tester, mais pour beaucoup de mes cas, cela aide beaucoup!
Chris Schaller
5

Je ne recommande pas d'utiliser l'attribut ExpectedException (car il est trop contraignant et sujet aux erreurs) ou d'écrire un bloc try / catch dans chaque test (car il est trop compliqué et sujet aux erreurs). Utilisez une méthode d'assertion bien conçue - fournie par votre framework de test ou écrivez la vôtre. Voici ce que j'ai écrit et utilisé.

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

Exemple utilise:

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

REMARQUES

Renvoyer l'exception au lieu de prendre en charge un rappel de validation est une idée raisonnable, sauf que cela rend la syntaxe d'appel de cette assertion très différente de celle des autres assertions que j'utilise.

Contrairement à d'autres, j'utilise «propage» au lieu de «lancers» car nous ne pouvons tester que si une exception se propage à partir d'un appel. Nous ne pouvons pas tester directement qu'une exception est levée. Mais je suppose que vous pourriez dire que les lancers d'images sont: lancés et non attrapés.

PENSÉE FINALE

Avant de passer à ce type d'approche, j'ai envisagé d'utiliser l'attribut ExpectedException lorsqu'un test ne vérifiait que le type d'exception et d'utiliser un bloc try / catch si une validation supplémentaire était requise. Mais, non seulement je devrais réfléchir à la technique à utiliser pour chaque test, mais changer le code d'une technique à l'autre en fonction des besoins n'était pas un effort trivial. L'utilisation d'une approche cohérente économise l'effort mental.

Donc en résumé, cette approche sportive: facilité d'utilisation, flexibilité et robustesse (difficile de se tromper).

Steve
la source
4

L'assistant fourni par @Richiban ci-dessus fonctionne très bien, sauf qu'il ne gère pas la situation où une exception est levée, mais pas le type attendu. Les réponses suivantes:

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
la source
2
Hmmm ... Je comprends l'idée, mais je ne suis pas sûr d'être d'accord, c'est mieux. Ce n'est pas parce que nous voulons nous assurer qu'une exception spécifique est déclenchée que toutes les autres doivent être considérées comme un échec d'assertion. À mon humble avis, une exception inconnue devrait simplement remonter la pile comme dans toute autre opération d'assertion.
Crono
@Martin Je supprimerais le code impliquant exceptionOther et je retomberais simplement de la deuxième clause catch
Tom Lint
4

Puisque vous mentionnez l'utilisation d'autres classes de test, une meilleure option que l' ExpectedExceptionattribut est d'utiliser Shoudly 's Should.Throw .

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

Disons que nous avons une exigence que le client doit avoir une adresse pour créer une commande . Sinon, la CreateOrderForCustomerméthode devrait aboutir à un ArgumentException. Ensuite, nous pourrions écrire:

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

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

C'est mieux que d'utiliser un ExpectedException attribut, car nous sommes précis sur ce qui devrait déclencher l'erreur. Cela rend les exigences de nos tests plus claires et facilite également le diagnostic lorsque le test échoue.

Notez qu'il existe également un Should.ThrowAsynctest de méthode asynchrone.

Justin J Stark
la source
4

Comme alternative, vous pouvez essayer de tester les exceptions qui sont en fait lancées avec les 2 lignes suivantes de votre test.

var testDelegate = () => MyService.Method(params);
Assert.Throws<Exception>(testDelegate);
Matias
la source
4

Dans les tests unitaires intégrés VS si vous voulez simplement vérifier que "toute exception" est levée, mais que vous ne connaissez pas le type, vous pouvez utiliser un catch all:

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

Eh bien, je vais résumer à peu près ce que tout le monde ici a dit avant ... Quoi qu'il en soit, voici le code que j'ai construit en fonction des bonnes réponses :) Tout ce qu'il reste à faire est de copier et d'utiliser ...

/// <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
la source
2

Consultez nUnit Docs pour des exemples sur:

[ExpectedException( typeof( ArgumentException ) )]
Jon Masters
la source
2

Cela dépendra du cadre de test que vous utilisez?

Dans MbUnit, par exemple, vous pouvez spécifier l'exception attendue avec un attribut pour vous assurer d'obtenir l'exception que vous attendez vraiment.

[ExpectedException(typeof(ArgumentException))]
Geai
la source
2

En cas d'utilisation de NUnit , essayez ceci:

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

Il existe une bibliothèque géniale appelée NFluent qui accélère et facilite la façon dont vous écrivez vos assertions .

Il est assez simple d'écrire une assertion pour lever une exception:

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

Même s'il s'agit d'une vieille question, je voudrais ajouter une nouvelle réflexion à la discussion. J'ai étendu le modèle Arrange, Act, Assert à prévoir, Arrange, Act, Assert. Vous pouvez créer un pointeur d'exception attendu, puis affirmer qu'il a été affecté à. Cela semble plus propre que de faire vos Asserts dans un bloc catch, laissant votre section Act principalement juste pour la seule ligne de code pour appeler la méthode en cours de test. Vous n'avez pas non plus à aller Assert.Fail();ou à returnpartir de plusieurs points dans le code. Toute autre exception levée entraînera l'échec du test, car il ne sera pas intercepté, et si une exception de votre type attendu est levée, mais ce n'était pas celle que vous attendiez, Asserting contre le message ou d'autres propriétés de l'exception permet de vous assurer que votre test ne passera pas par inadvertance.

[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
la source