Meilleur moyen de tester les exceptions avec Assert pour s'assurer qu'elles seront levées

97

Pensez-vous que c'est un bon moyen de tester les exceptions? Aucune suggestion?

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

Assert.IsNotNull(exception);

J'utilise MS Test.

Hannoun Yassir
la source

Réponses:

137

J'ai plusieurs modèles différents que j'utilise. J'utilise l' ExpectedExceptionattribut la plupart du temps lorsqu'une exception est attendue. Cela suffit dans la plupart des cas, cependant, dans certains cas, cela ne suffit pas. L'exception peut ne pas être capturable - puisqu'elle est lancée par une méthode qui est invoquée par réflexion - ou peut-être que je veux simplement vérifier que d'autres conditions sont respectées, disons qu'une transaction est annulée ou qu'une valeur a toujours été définie. Dans ces cas, je l'enveloppe dans un try/catchbloc qui attend l'exception exacte, fait un Assert.Failsi le code réussit et intercepte également les exceptions génériques pour m'assurer qu'une exception différente n'est pas levée.

Premier cas:

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

Deuxième cas:

[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
la source
16
De nombreux frameworks de test unitaire implémentent les échecs d'assertion comme exceptions. Ainsi, Assert.Fail () dans le second cas sera intercepté par le bloc catch (Exception), qui masquera le message d'exception. Vous devez ajouter une capture (NUnit.Framework.AssertionException) {throw;} ou similaire - voir ma réponse.
GrahamS
@Graham - J'ai tapé ça du haut de ma tête. Normalement, j'imprimerais également le message d'exception en plus de son type. Le fait est que le test échouerait puisque le second gestionnaire intercepterait l'échec d'assertion et "refail" avec des informations sur l'erreur.
tvanfosson le
1
Bien que votre code soit fonctionnel, 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 c'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. Vous pouvez obtenir un meilleur code et vous n'aurez pas à choisir entre les différentes techniques ou à passer de l'une à l'autre à mesure que le test change. Voir stackoverflow.com/a/25084462/2166177
steve
FYI - Je suis passé à l'utilisation de xUnit qui a une Assert.Throwsméthode fortement typée qui couvre ces deux cas.
tvanfosson
L'attribut ExpectedException est une manière désagréable et datée de tester si des exceptions sont levées. Voir ma réponse complète ci-dessous.
bytedev
43

Maintenant, 2017, vous pouvez le faire plus facilement avec le nouveau Framework MSTest V2 :

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

//async version
await Assert.ThrowsExceptionAsync<SomeException>(
  () => myObject.SomeMethodAsync()
);
Icaro Bombonato
la source
Cela ne réussira que si un System.Exceptionest lancé. Tout autre, comme un System.ArgumentExceptionéchouera au test.
sschoof
2
Si vous attendez un autre type d'exception, vous devriez le tester ... Dans votre exemple, vous devriez faire: Assert.ThrowsException <ArgumentException> (() => myClass.MyMethodWithError ());
Icaro Bombonato
2
Il est important de noter que l'utilisation de Assert.ThrowsException<MyException>testera uniquement par rapport au type d'exception fourni, et non à l'un de ses types d'exception dérivés. Dans mon exemple, si le test Subétait à Throwun MyInheritedException(type dérivé de la classe de base MyException), le test échouerait .
Ama
Si vous souhaitez élargir votre test et accepter un type d'exception ainsi que ses types dérivés, utilisez un Try { SubToTest(); Assert.Fail("...") } Catch (AssertFailedException e) {throw;} Catch (MyException e) {...}. Notez la plus haute importance de Catch (AssertFailedException e) {throw;}(cf. commentaire de allgeek)
Ama
17

Je suis nouveau ici et je n'ai pas la réputation de commenter ou de voter contre, mais je voulais souligner une faille dans l'exemple de la réponse d'Andy White :

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

Dans tous les frameworks de tests unitaires que je connais, Assert.Failfonctionne en lançant une exception, donc le catch générique masquera en fait l'échec du test. Si SomethingThatCausesAnException()ne lance pas, la Assert.Failvolonté, mais cela ne sera jamais envoyé au testeur pour indiquer un échec.

Si vous avez besoin d'attraper l'exception attendue (c'est-à-dire pour affirmer certains détails, comme le message / les propriétés sur l'exception), il est important d'attraper le type attendu spécifique, et non la classe Exception de base. Cela permettrait à l' Assert.Failexception de sortir (en supposant que vous ne lançiez pas le même type d'exception que votre cadre de test unitaire), mais permettrait toujours la validation de l'exception qui a été levée par votre SomethingThatCausesAnException()méthode.

allgeek
la source
15

À partir de la version 2.5, NUnit a les niveaux de méthode suivants Assertpour tester les exceptions:

Assert.Throws , qui testera un type d'exception exact:

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

Et Assert.Catch, qui testera une exception d'un type donné, ou un type d'exception dérivé de ce type:

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

En passant, lors du débogage de tests unitaires qui lèvent des exceptions, vous souhaiterez peut-être empêcher VS de rompre sur l'exception .

Éditer

Juste pour donner un exemple du commentaire de Matthew ci-dessous, le retour du générique Assert.Throwset Assert.Catchest l'exception avec le type de l'exception, que vous pouvez ensuite examiner pour une inspection plus approfondie:

// The type of ex is that of the generic type parameter (SqlException)
var ex = Assert.Throws<SqlException>(() => MethodWhichDeadlocks());
Assert.AreEqual(1205, ex.Number);
StuartLC
la source
2
Roy Osherove le recommande dans The Art of Unit Testing, deuxième éd., Section 2.6.2.
Avi
2
J'aime Assert.Throws, en plus il renvoie l'exception pour que vous puissiez écrire d'autres assertions sur l'exception elle-même.
Matthew
La question était pour MSTest pas NUnit.
bytedev
La question initiale de @nashwan OP n'avait pas cette qualification, et le balisage ne qualifie toujours pas MS-Test. Dans l'état actuel des choses, il s'agit d'une question C #, .Net, Unit-Testing.
StuartLC
11

Malheureusement, MSTest TOUJOURS n'a vraiment que l'attribut ExpectedException (montre juste à quel point MS se soucie de MSTest) quel IMO est assez horrible car il rompt le modèle Arrange / Act / Assert et ne vous permet pas de spécifier exactement quelle ligne de code vous attendez à l'exception se produire.

Lorsque j'utilise (/ forcé par un client) pour utiliser MSTest, j'utilise toujours cette classe d'assistance:

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

Exemple d'utilisation:

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

Au lieu d'utiliser l' ExpectedExceptionattribut, je définis parfois deux méthodes utiles pour mes classes de test:

AssertThrowsException() prend un délégué et affirme qu'il lève l'exception attendue avec le message attendu.

AssertDoesNotThrowException() prend le même délégué et affirme qu'il ne lève pas d'exception.

Ce jumelage peut être très utile lorsque vous souhaitez tester qu'une exception est levée dans un cas, mais pas dans l'autre.

En les utilisant, mon code de test unitaire pourrait ressembler à ceci:

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

Sympa et soigné hein?

Les méthodes My AssertThrowsException()et AssertDoesNotThrowException()sont définies sur une classe de base commune comme suit:

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

Marquez le test avec ExpectedExceptionAttribute (c'est le terme dans NUnit ou MSTest; les utilisateurs d'autres frameworks de test unitaire peuvent avoir besoin de traduire).

Itowlson
la source
N'utilisez pas ExpectedExceptionAttribute (raison donnée dans mon message ci-dessous). NUnit a Assert.Throws <YourException> () et pour MSTest, utilisez quelque chose comme ma classe AssertException ci-dessous.
bytedev
3

Avec la plupart des frameworks de test unitaire .net, vous pouvez mettre un attribut [ExpectedException] sur la méthode de test. Cependant, cela ne peut pas vous dire que l'exception s'est produite au moment où vous vous y attendiez. C'est là que xunit.net peut vous aider.

Avec xunit, vous avez Assert.Throws, vous pouvez donc faire des choses comme ceci:

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

[Fact] est l'équivalent xunit de [TestMethod]

Steve Willcock
la source
Si vous devez utiliser MSTest (ce que les employeurs m'obligent souvent), voyez ma réponse ci-dessous.
bytedev
0

Suggérer d 'utiliser la syntaxe de délégué propre de NUnit .

Exemple de test 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
la source