关于c#:如何使用Assert来验证是否抛出了异常?

How do I use Assert to verify that an exception has been thrown?

如何使用断言(或其他测试类?)验证是否引发了异常?


对于"Visual Studio团队测试",您似乎将ExpectedException属性应用于测试的方法。

本文中的示例:带有Visual Studio团队测试的单元测试演练

1
2
3
4
5
6
7
[TestMethod]
[ExpectedException(typeof(ArgumentException),
   "A userId of null was inappropriately allowed.")]
public void NullUserIdInConstructor()
{
   LogonInfo logonInfo = new LogonInfo(null,"P@ss0word");
}


通常,您的测试框架会对此给出答案。但是如果它不够灵活,你可以一直这样做:

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

正如@jonas指出的,这不适用于捕获基本异常:

1
2
3
4
5
6
try {
    somethingThatShouldThrowAnException();
    Assert.Fail(); // raises AssertionException
} catch (Exception) {
    // Catches the assertion exception, and the test passes
}

如果必须捕获异常,则需要重新引发assert.fail()。但实际上,这是一个标志,您不应该手工编写这个标志;检查您的测试框架中的选项,或者看看您是否可以抛出一个更有意义的异常进行测试。

1
catch (AssertionException) { throw; }

您应该能够使这种方法适应您喜欢的任何东西——包括指定要捕获的异常类型。如果您只希望使用某些类型,请使用以下命令完成catch块:

1
2
3
4
5
} catch (GoodException) {
} catch (Exception) {
    // not the right kind of exception
    Assert.Fail();
}


实现这一点的首选方法是编写一个名为throw的方法,并像其他断言方法一样使用它。不幸的是,.NET不允许您编写静态扩展方法,因此您不能将此方法当作它实际上属于内置断言类来使用;只需将另一个方法称为MyAssert或类似的方法。类如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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))
                    );
            }
        }
    }
}

这意味着您的单元测试如下所示:

1
2
3
4
5
6
[TestMethod()]
public void ExceptionTest()
{
    String testStr = null;
    MyAssert.Throws<NullReferenceException>(() => testStr.ToUpper());
}

它看起来和行为更像单元测试语法的其余部分。


如果您使用的是mstest,它最初没有ExpectedException属性,那么可以这样做:

1
2
3
4
5
6
7
8
9
try
{
    SomeExceptionThrowingMethod()
    Assert.Fail("no exception thrown");
}
catch (Exception ex)
{
    Assert.IsTrue(ex is SpecificExceptionType);
}


如果你使用Nunit,你可以这样做:

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

还可以存储引发的异常,以便进一步验证它:

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

请参阅:http://nunit.org/docs/2.5/exceptionasserts.html


小心使用预期感受,因为它可能导致以下几个陷阱:

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

这里:

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

如果您需要测试异常情况,那么就少了对方法的不满。您可以使用try act/fail catch assert方法,这对于除了ExpectedException之外没有直接支持异常测试的框架很有用。

另一个更好的选择是使用xunit.net,它是一个非常现代、前瞻性和可扩展的单元测试框架,从所有其他错误中吸取教训并加以改进。其中一个改进是assert.throws,它为断言异常提供了更好的语法。

您可以在github上找到xunit.net:http://xunit.github.io/


在我正在进行的一个项目中,我们有另一个解决方案。

首先,我不喜欢ExpectedExceptionAttribute,因为它确实考虑了导致异常的方法调用。

我用帮助方法来代替。

试验

1
2
3
4
5
6
7
8
9
10
[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());            
}

螺旋法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
    }

整洁,不是吗;)


MSTEST(v2)现在有一个assert.throwsException函数,可以这样使用:

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

你可以用nuget:Install-Package MSTest.TestFramework安装


它是测试方法的一个属性…您不使用断言。如下所示:

1
2
[ExpectedException(typeof(ExceptionType))]
public void YourMethod_should_throw_exception()


您可以使用:pm>安装包mstestextensions从nuget下载包,该包以nunit/xunit的样式向mstest添加assert.throws()语法。

高级说明:下载程序集并从basetest继承,您可以使用assert.throws()语法。

throws实现的主要方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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)));
    }
}

披露:我把这个包裹放在一起。

更多信息:http://www.bradoncode.com/blog/2012/01/asserting-exceptions-in-mstest-with.html


您可以用一条简单的单行线来实现这一点。

如果您的操作foo.bar()是异步的:

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

如果foo.bar()不是异步的

1
Assert.ThrowsException<Exception>(() => foo.bar());


我不建议使用expectedException属性(因为它太约束和容易出错),也不建议在每个测试中编写一个try/catch块(因为它太复杂和容易出错)。使用一个设计良好的断言方法——要么由测试框架提供,要么编写自己的方法。这是我写的和使用的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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));
    }
}

示例用途:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    [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)
    }

笔记

返回异常而不支持验证回调是一个合理的想法,但这样做会使这个断言的调用语法与我使用的其他断言非常不同。

与其他人不同,我使用"传播"而不是"抛出",因为我们只能测试异常是否从调用传播。我们不能直接测试是否引发了异常。但我想你可以想象一下投掷是指:投掷而不是捕获。

最后的想法

在切换到这种方法之前,我考虑使用expectedException属性,当测试仅验证异常类型时,如果需要更多验证,则使用try/catch块。但是,不仅我必须考虑在每个测试中使用哪种技术,而且根据需要的变化将代码从一种技术更改为另一种技术并不是一件容易的事情。使用一种一致的方法可以节省脑力劳动。

因此,总的来说,这种方法具有以下优点:易用性、灵活性和健壮性(很难出错)。


上面@richiban提供的帮助程序非常有效,除非它不处理引发异常的情况,但不处理预期的类型。以下地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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))
                    );
            }
        }
    }
}


作为另一种选择,您可以尝试测试异常,事实上,异常被抛出到您的测试中的下两行中。

1
2
var testDelegate = () => MyService.Method(params);
Assert.Throws<Exception>(testDelegate);


由于您提到使用其他测试类,比ExpectedException属性更好的选择是使用shoully的should.throw。

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

假设我们要求客户必须有地址才能创建订单。否则,CreateOrderForCustomer方法应生成ArgumentException。然后我们可以写:

1
2
3
4
5
6
7
8
[TestMethod]
public void NullUserIdInConstructor()
{
  var customer = new Customer(name :="Justin", address := null};

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

这比使用ExpectedException属性要好,因为我们对应该抛出错误的内容很明确。这使得我们的测试中的需求更加清晰,并且在测试失败时更容易诊断。

注:异步方法测试也有一个Should.ThrowAsync


在vs内置单元测试中,如果只想验证抛出了"any exception",但不知道类型,可以使用catch all:

1
2
3
4
5
6
[TestMethod]
[ExpectedException(typeof(Exception), AllowDerivedTypes = true)]
public void ThrowExceptionTest()
{
    //...
}

好吧,我会大致总结一下这里其他人之前说过的话……不管怎样,这是我根据好答案构建的代码:)剩下的工作就是复制和使用……

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/// <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.");  
}

查看nunit文档以获取以下示例:

1
[ExpectedException( typeof( ArgumentException ) )]

这将取决于您使用的是什么测试框架?

例如,在MBUnit中,可以使用属性指定预期的异常,以确保获得真正期望的异常。

1
[ExpectedException(typeof(ArgumentException))]


如果使用Nunit,请尝试以下操作:

1
2
3
4
Assert.That(() =>
        {
            Your_Method_To_Test();
        }, Throws.TypeOf<Your_Specific_Exception>().With.Message.EqualTo("Your_Specific_Message"));

尽管这是一个古老的问题,我还是想在讨论中添加一个新的想法。我已经将arrange、act、assert模式扩展为expected、arrange、act、assert。您可以创建一个预期的异常指针,然后断言它被分配给了。这比在catch块中执行断言更干净,只留下您的act部分,主要是为了让一行代码调用测试中的方法。您也不必从代码中的多个点对Assert.Fail();return进行操作。引发的任何其他异常都将导致测试失败,因为它不会被捕获,并且如果引发了预期类型的异常,但它不是您期望的异常,断言异常的消息或其他属性有助于确保测试不会在无意中通过。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[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);
}

有一个很棒的叫做nfluent的库,它可以加速和简化你写断言的方式。

编写引发异常的断言非常简单:

1
2
3
4
5
6
7
    [Test]
    public void given_when_then()
    {
        Check.ThatCode(() => MethodToTest())
            .Throws<Exception>()
            .WithMessage("Process has been failed");
    }