使用断言测试异常以确保将其抛出的最佳方法


97

您是否认为这是测试异常的好方法?有什么建议?

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

Assert.IsNotNull(exception);

我正在使用MS测试。

Answers:


137

我有几种不同的模式可以使用。我ExpectedException通常会在预期出现异常时使用该属性。这足以满足大多数情况,但是在某些情况下这还不够。异常可能不是可捕获的-因为它是由反射调用的方法抛出的-也许我只是想检查其他条件是否成立,比如说事务已回滚或仍设置了一些值。在这些情况下,我将其包装在try/catch需要确切异常Assert.Fail的代码块中,如果代码成功,则执行,并捕获通用异常以确保不会引发其他异常。

第一种情况:

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

第二种情况:

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

16
许多单元测试框架将断言失败实现为异常。因此,第二种情况下的Assert.Fail()将被catch(Exception)块捕获,该块将隐藏异常消息。您需要添加一个catch(NUnit.Framework.AssertionException){throw;}或类似内容-请参阅我的答案。
GrahamS 2009年

@Graham-我在脑海中打了这个字。通常,除了类型外,我还要打印出异常消息。关键是该测试将失败,因为第二个处理程序将捕获断言失败并通过错误信息“ refail”。
tvanfosson

1
尽管您的代码在功能上是合理的,但我不建议您使用ExpectedException属性(因为它太受约束且容易出错),也不建议在每个测试中编写try / catch块(因为它太复杂且容易出错)。使用设计良好的assert方法-由测试框架提供或编写自己的方法。您可以获得更好的代码,而不必在不同的技术之间进行选择,也不必随着测试的变化而从一种技术转换为另一种技术。请参阅stackoverflow.com/a/25084462/2166177
2014年

仅供参考-我已经开始使用xUnit,它具有Assert.Throws涵盖这两种情况的强类型 方法。
tvanfosson

ExpectedException属性是一种讨厌且过时的方法,用于测试是否引发异常。请参阅下面的完整答案。
bytedev '16

45

现在,2017年,您可以使用新的MSTest V2 Framework轻松完成此工作:

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

//async version
await Assert.ThrowsExceptionAsync<SomeException>(
  () => myObject.SomeMethodAsync()
);

仅当System.Exception抛出an时才会成功。其他任何项目(如)System.ArgumentException都将无法通过测试。
sschoof19年

2
如果您期望其他类型的异常,则应该对其进行测试...在您的示例中,应执行以下操作:Assert.ThrowsException <ArgumentException>(()=> myClass.MyMethodWithError());
伊卡洛·邦博纳托

2
需要注意的重要一点是,Assert.ThrowsException<MyException>will 的使用仅针对提供的异常类型进行测试,而不是针对其派生的异常类型进行测试。在我的例子中,如果所测试的SubThrow一个MyInheritedException(派生从基类型MyException),则测试将失败
Ama

如果您想扩展测试并接受异常类型及其派生类型,请使用Try { SubToTest(); Assert.Fail("...") } Catch (AssertFailedException e) {throw;} Catch (MyException e) {...}。请注意Catch (AssertFailedException e) {throw;}(参见allgeek的评论)的最高重要性
Ama,

16

我是新来的,没有发表评论或低票的声誉,但想指出安迪·怀特(Andy White)答复示例中的一个缺陷:

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

在我熟悉的所有单元测试框架中,均会Assert.Fail引发异常,因此通用捕获实际上将掩盖测试的失败。如果SomethingThatCausesAnException()没有抛出Assert.Fail遗嘱,但是那将永远不会冒出来给测试运行者以表明失败。

如果您需要捕获预期的异常(即,声明某些详细信息,例如异常的消息/属性),那么捕获特定的预期类型而不是基础Exception类非常重要。这将允许Assert.Fail异常冒出来(假设您未抛出与单元测试框架相同的异常类型),但仍允许对SomethingThatCausesAnException()方法抛出的异常进行验证。


15

从v 2.5开始,NUnit具有以下方法级别Assert用于测试异常的:

Assert.Throws,它将测试确切的异常类型:

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

Assert.Catch,它将测试给定类型的异常或从该类型派生的异常类型:

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

顺便说一句,在调试引发异常的单元测试时,您可能要防止VS 打破异常

编辑

仅举例来说,下面是Matthew的评论,泛型Assert.Throws和的返回 Assert.Catch是具有异常类型的异常,然后可以对其进行检查以进行进一步检查:

// The type of ex is that of the generic type parameter (SqlException)
var ex = Assert.Throws<SqlException>(() => MethodWhichDeadlocks());
Assert.AreEqual(1205, ex.Number);

2
Roy Osherove在“单元测试的艺术”,第二版,第2.6.2节中建议这样做。
2014年

2
我喜欢Assert.Throws,此外它还返回异常,因此您可以在异常本身上编写进一步的断言。
马修

问题是针对MSTest而不是NUnit。
bytedev '17

@nashwan OP最初的问题不具备该资格,并且标记仍不符合MS-Test。就目前而言,这是一个C#、. Net,单元测试问题。
StuartLC'4

11

不幸的是,MSTest STILL实际上仅具有ExpectedException属性(仅显示了MS对MSTest的关心程度),该IMO非常糟糕,因为它破坏了Arrange / Act / Assert模式,并且不允许您确切指定期望异常的代码行发生。

当我使用(/由客户端强制)使用MSTest时,我总是使用以下帮助程序类:

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

用法示例:

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

10

作为使用ExpectedException属性的替代方法,有时我为测试类定义两个有用的方法:

AssertThrowsException() 接受一个委托并断言它使用预期的消息引发了预期的异常。

AssertDoesNotThrowException() 接受相同的委托并断言它不会引发异常。

当您要测试一种情况下引发异常而另一种情况下引发异常时,此配对非常有用。

使用它们,我的单元测试代码可能看起来像这样:

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

干净利落吧?

AssertThrowsException()AssertDoesNotThrowException()方法是在通用基类上定义的,如下所示:

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

4

对于大多数.net单元测试框架,您可以在测试方法上放置[ExpectedException]属性。但是,这不能告诉您异常发生在您期望的时间点。这就是xunit.net可以提供帮助的地方。

使用xunit可以拥有Assert.Throws,因此您可以执行以下操作:

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

[事实]是[TestMethod]的xunit等效项


如果您必须使用MSTest(雇主经常强迫我使用),请在下面查看我的答案。
bytedev '17

4

用ExpectedExceptionAttribute标记测试(这是NUnit或MSTest中的术语;其他单元测试框架的用户可能需要翻译)。


不要使用ExpectedExceptionAttribute(以下我的帖子中给出的原因)。NUnit具有Assert.Throws <YourException>(),对于MSTest,请使用下面的AssertException类。
bytedev '17

0

建议使用NUnit的干净委托语法。

测试示例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>);
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.