使用 Assert 测试异常以确保它们被抛出的最佳方法

Posted

技术标签:

【中文标题】使用 Assert 测试异常以确保它们被抛出的最佳方法【英文标题】:Best way to test exceptions with Assert to ensure they will be thrown 【发布时间】:2010-10-18 23:14:16 【问题描述】:

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

Exception exception = null;
try
    //I m sure that an exeption will happen here

catch (Exception ex)
    exception = ex;


Assert.IsNotNull(exception);

我正在使用 MS 测试。

【问题讨论】:

【参考方案1】:

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

【讨论】:

不要使用 ExpectedExceptionAttribute(原因在我下面的帖子中给出)。 NUnit 有 Assert.Throws() 并且对于 MSTest 使用类似于我下面的 AssertException 类的东西。【参考方案2】:

对于大多数 .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));
    

[Fact] 是 [TestMethod] 的 xunit 等价物

【讨论】:

如果您必须使用 MSTest(我经常被雇主强迫使用),请在下面查看我的答案。【参考方案3】:

我使用了几种不同的模式。大多数情况下,我会使用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 )
        );
    

【讨论】:

许多单元测试框架将断言失败作为异常来实现。所以第二种情况下的 Assert.Fail() 将被 catch(Exception) 块捕获,这将隐藏异常消息。您需要添加一个 catch(NUnit.Framework.AssertionException)throw; 或类似的 - 请参阅我的答案。 @Graham -- 我在脑海中输入了这个。通常,除了类型之外,我还会打印出异常消息。关键是测试会失败,因为第二个处理程序会捕获断言失败并“重新失败”并提供有关错误的信息。 虽然你的代码在功能上是合理的,但我不推荐使用 ExpectedException 属性(因为它太受约束且容易出错)或在每个测试中编写一个 try/catch 块(因为它太复杂和错误-易于)。使用设计良好的断言方法——要么由您的测试框架提供,要么自己编写。您可以获得更好的代码,并且您不必在不同的技术之间进行选择,也不必随着测试的变化而从一种技术转换到另一种技术。见***.com/a/25084462/2166177 仅供参考 - 我已经开始使用 xUnit,它具有涵盖这两种情况的强类型 Assert.Throws 方法。 ExpectedException 属性是测试是否抛出异常的讨厌且过时的方法。请参阅下面的完整答案。【参考方案4】:

作为使用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);
    

【讨论】:

【参考方案5】:

我是新来的,没有评论或否决的声誉,但想指出 Andy White's reply 中示例中的一个缺陷:

try

    SomethingThatCausesAnException();
    Assert.Fail("Should have exceptioned above!");

catch (Exception ex)

    // whatever logging code

在我熟悉的所有单元测试框架中,Assert.Fail 通过抛出异常来工作,因此通用 catch 实际上会掩盖测试失败。如果SomethingThatCausesAnException() 没有抛出,Assert.Fail 会抛出,但这永远不会冒泡给测试运行器以指示失败。

如果您需要捕获预期的异常(即,断言某些细节,例如异常的消息/属性),捕获特定的预期类型很重要,而不是基 Exception 类。这将允许Assert.Fail 异常冒泡(假设您没有抛出与单元测试框架相同类型的异常),但仍然允许对SomethingThatCausesAnException() 方法抛出的异常进行验证。

【讨论】:

【参考方案6】:

编辑: MS Test Framework 迟早地复制了其他单元测试框架,现在确实有Assert.ThrowsException and Assert.ThrowsExceptionAsync,其行为类似于 NUnit 等效项。

但是,在撰写本文时,仍然没有直接等效的 Assert.Catch&lt;TException&gt; 允许测试 TExceptionTException 的子类,因此您的单元测试需要准确设置和例外被测试。来自 MS 测试文档:

测试委托动作指定的代码是否准确抛出类型 T (而不是派生类型)的给定异常,如果代码没有抛出异常或抛出非 T 类型的异常,则抛出 AssertFailedException。

SDK 2017 之前

MS 需要赶上其他测试框架中可用的功能。例如从 v 开始,2.5, NUnit 具有以下方法级 Asserts 用于测试异常:

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

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

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

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

顺便说一句,在调试抛出异常的单元测试时,您可能希望阻止 VS 访问breaking on the exception。

编辑

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

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

【讨论】:

Roy Osherove 在《单元测试的艺术》第二版第 2.6.2 节中推荐了这一点。 我喜欢Assert.Throws,此外它还返回异常,因此您可以在异常本身上编写进一步的断言。 问题是针对 MSTest 而不是 NUnit。 @nashwan OP 的原始问题没有那个资格,并且标记仍然没有资格 MS-Test。就目前而言,这是一个 C#、.Net、单元测试问题。【参考方案7】:

不幸的是,MSTest 仍然只有 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));

【讨论】:

【参考方案8】:

现在,2017 年,您可以使用新的MSTest V2 Framework 更轻松:

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

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

【讨论】:

只有在抛出 System.Exception 时才会成功。任何其他的,比如System.ArgumentException 都将无法通过测试。 如果你期待另一种类型的异常,你应该测试它......在你的例子中,你应该这样做: Assert.ThrowsException(() => myClass.MyMethodWithError()) ; 需要注意的重要一点是Assert.ThrowsException&lt;MyException&gt; 的使用将仅针对所提供的异常类型进行测试,而不是针对其派生的任何异常类型进行测试。在我的示例中,如果测试的SubThrowMyInheritedException(从基类MyException 派生的类型),那么测试将失败 如果您想扩展您的测试并接受异常类型及其派生类型,请使用Try SubToTest(); Assert.Fail("...") Catch (AssertFailedException e) throw; Catch (MyException e) ...。注意Catch (AssertFailedException e) throw; 的最高重要性(参见 allgeek 的评论)【参考方案9】:

建议使用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>);

【讨论】:

【参考方案10】:

这是我在调用 EnsureSuccessStatusCode 扩展方法时测试抛出的 HttpRequestException 所做的(.NET Core 3.1 MS 测试):

var result = await Assert.ThrowsExceptionAsync<HttpRequestException>(async ()=>

    await myService.SomeMethodAsync("test value");


Assert.AreEqual("Response status code does not indicate success: 401 (Unauthorized).", result);

上面测试了 SomeMethodAsync 方法是否抛出 T 类型的异常,在这种情况下 HttpRequestException 然后我可以做更多的断言,例如测试它不是null,是HttpRequestException 类型,并且异常消息与上面示例中的401 Unauthorised 字符串匹配(HttpRequestException 基于Exception 类,因此您可以访问它的所有属性和方法)。

【讨论】:

以上是关于使用 Assert 测试异常以确保它们被抛出的最佳方法的主要内容,如果未能解决你的问题,请参考以下文章

java 标准异常

使用 sbt 和 testng 时,如何获取测试中抛出的异常的完整堆栈跟踪?

Java——标准异常

使用 Laravel 5.2 的 PHPUnit 未找到自定义异常

通过异常处理错误-2

学习笔记