如何使用Assert验证是否已引发异常?


830

如何使用Assert(或其他Test类?)验证是否已引发异常?


您正在使用哪个单元测试框架?
凯文·普林

3
集成的Visual Studio
Alex

4
ExpectedException属性没有帮助吗?参考:msdn.microsoft.com/en-us/library/...
shahkalpesh

2
有趣的是,我刚刚找到答案了,在stackoverflow.com/questions/741029/testing-exceptions中找到了它。
dfjacobs,2009年

Answers:


978

对于“ Visual Studio Team Test”,您似乎将ExpectedException属性应用于该测试的方法。

这里的文档样本:使用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");
}

25
上面的ExpectedException属性也可以在NUnit中使用(但[TestMethod]应该为[Test])。
dbkk'2009年

5
@dbkk:在NUnit中的工作原理不完全相同-消息被视为需要匹配异常消息的字符串(IU认为这更有意义)
Ruben Bartelink 2009年

29
此属性可以完成工作,并且是c#程序员的内置功能,但是我不建议使用它,因为它不够灵活。请考虑一下,如果您的测试设置代码抛出了异常类型:测试通过,但实际上没有达到您的期望,该怎么办。或者,如果您想测试异常对象的状态,该怎么办。我通常想使用StringAssert.Contains(e.Message ...),而不是测试整个消息。使用其他答案中所述的assert方法。
2014年

3
避免在NUnit中使用ExpectedException,因为它将在NUnit 3.0中删除。我更喜欢使用Assert.Throws <SpecificException>()
特伦斯

5
您可以在MsTest中使用Assert.ThrowsException <T>和Assert.ThrowsExceptionAsync <T>。
Gopal Krishnan

257

通常,您的测试框架会对此提供答案。但是,如果它不够灵活,可以随时执行以下操作:

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

正如@Jonas指出的那样,这对于捕获基本异常无效:

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

如果绝对必须捕获Exception,则需要重新抛出Assert.Fail()。但是,实际上,这是一个不应该手写的迹象。检查测试框架中的选项,或者查看是否可以抛出更有意义的异常以进行测试。

catch (AssertionException) { throw; }

您应该能够使此方法适应您喜欢的任何方式-包括指定要捕获的异常类型。如果只希望使用某些类型,请完成以下步骤catch

} catch (GoodException) {
} catch (Exception) {
    // not the right kind of exception
    Assert.Fail();
}

20
+1,当我需要进行除异常类型之外的断言时,我使用这种方式而不是属性。例如,如果需要检查将异常实例中的某些字段设置为某些值,该怎么办。
Pavel Repin

2
您不需要指定错误消息。这就足够了:[ExpectedException(typeof(ArgumentException))]
mibollma

5
我认为这种解决方案是最好的。如果测试很简单,[ExpectedException(typeof(ArgumentException)))就会有用,但是我认为这是一个懒惰的解决方案,对它的适应会导致陷阱。该解决方案为您提供了进行更正确测试的特定控制,此外,您还可以在测试运行报告的测试Writeline中测试该异常确实按预期引发。
evilfish 2013年

12
请谨慎操作,因为Assert.Fail()引发异常,如果您捕获到异常,则测试通过!
乔纳斯(Jonas)2013年

4
@ Vinnyq12我的意思是上面示例中的第一个测试将永远不会失败。测试失败,如果一个异常被抛出(而不是“捕获”由ExpectedExceptionAttribute)
乔纳斯

113

实现此目的的首选方法是编写一个称为Throws的方法,并像其他任何Assert方法一样使用它。不幸的是,.NET不允许您编写静态扩展方法,因此您无法像使用该方法实际上属于Assert类中的内部版本一样使用该方法。只需创建另一个名为MyAssert或类似名称的文件即可。该类如下所示:

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

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

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

它的外观和行为更像其余的单元测试语法。


1
摆脱bool标志,并在调用之后直接将代码置于行上,以实现更紧凑的实现。
gt

11
唯一使事情变得更好的方法是让函数返回捕获的异常,以便您可以继续断言诸如该异常的属性之类的东西是正确的。
Mark Hildreth

2
谢谢!这对我来说似乎是最好的方法,因为这是在一种方法中测试多个异常的简短方法。它也更具可读性。
David Sherret 2014年

2
@MickeyPerlstein属性违反了AAA测试规则。具体来说,如果您的安排恰好在您未执行该法案之前就引发了异常,那么您的测试就通过了!
freen-m

2
微软终于可以全面更新MSTest-v2支持Assert.ThrowsException<T>Assert.ThrowsExceptionAsync<T>-参见blogs.msdn.microsoft.com/visualstudioalm/2017/02/25/…–
Quango

62

如果您使用NUNIT,则可以执行以下操作:

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


还可以存储抛出的异常以便进一步验证它:

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


60

如果您使用的是最初没有ExpectedException属性的MSTest,则可以执行以下操作:

try 
{
    SomeExceptionThrowingMethod()
    Assert.Fail("no exception thrown");
}
catch (Exception ex)
{
    Assert.IsTrue(ex is SpecificExceptionType);
}

3
这确实可行,但由于逻辑过于复杂,因此一般不建议这样做。并不是说它是令人费解的,而是考虑是否为多个测试编写此代码块-10s,100s测试。需要将此逻辑移植到设计良好的assert方法中。查看其他答案。
2014年

35

谨慎使用ExpectedException,因为它可能导致以下陷阱:

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/


4
请注意,NUnit 2.5现在也支持Assert.Throws样式语法-nunit.com/index.php?p=releaseNotes&r=2.5
Alconja,2009年

在使用ExpectedException时,单元测试停止让您知道异常的方式使我发疯。为什么MS认为手动进行自动化测试是一个好主意?感谢您的链接。
蚂蚁

@Ant:MS复制了NUnit ...所以真正的问题是,为什么NUnit认为这是个好主意?
jrista 2011年

28

MSTest(v2)现在具有一个Assert.ThrowsException函数,可以像这样使用:

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

您可以使用nuget安装它: Install-Package MSTest.TestFramework


在2018年,这被认为是最佳实践,因为它仅检查被测单元是否在抛出而不检查其他代码。
CM

24

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

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

我使用辅助方法来代替。

测试

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

辅助方法

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

整洁,不是吗;)


14

它是测试方法的一个属性...您不使用Assert。看起来像这样:

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

13

您可以使用以下方法从Nuget下载软件包:PM> Install-Package MSTestExtensions,它以nUnit / xUnit样式向MsTest 添加了Assert.Throws()语法。

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

Throws实现的主要方法如下所示:

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


谢谢你的例子。您是否有一个示例如何测试Assert.DoesNotThrow()或等效示例?
2015年

10

您可以通过简单的一行实现。

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

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

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

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

1
还有很多其他答案,对我来说,我正在寻找一种仅通过“异常类型”测试已知失败条件的简便方法,这使最容易阅读的测试用例成为可能。注意:异常类型与继承的异常类(例如标准try-catch)不匹配,因此上述示例将不会捕获ArgumentException实例。如果您有要测试的高级标准,仍然首选旧的Try Catch和测试异常响应,但是对于我的很多情况,这很有帮助!
克里斯·谢勒

5

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

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

示例使用:

    [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块。但是,我不仅要考虑为每个测试使用哪种技术,而且随着需求的变化将代码从一种技术更改为另一种技术并不是一件容易的事。使用一种一致的方法可以节省精力。

因此,总而言之,这种方法具有运动性:易用性,灵活性和健壮性(很难做到错误)。


4

上面的@Richiban提供的帮助程序可以很好地工作,除了它不能处理引发异常的情况,而不是预期的类型。以下说明:

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

2
嗯...我知道这个主意,但是我不确定我同意会更好。仅仅因为我们要确保引发特定的异常并不意味着所有其他异常都应该被包装为断言失败。恕我直言,一个未知的异常应该像在任何其他断言操作中一样,使堆栈冒泡。
克鲁诺

@Martin我将删除涉及exceptionOther的代码,并从第二个catch子句中重新抛出
Tom Lint 2014年

4

由于您提到使用其他测试类,因此比该ExpectedException属性更好的选择是使用ShoudlyShould.Throw

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

假设我们有一个要求,客户必须具有创建订单地址。如果不是,则该方法将导致。然后我们可以写:CreateOrderForCustomerArgumentException

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

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

这比使用 ExpectedException属性因为我们要具体说明应该引发什么错误。这使我们的测试要求更加清晰,并且在测试失败时也使诊断更加容易。

注意,还有一个Should.ThrowAsync用于异步方法测试的方法。


4

作为替代方案,您可以尝试测试,实际上测试中的下两行会引发异常。

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

4

在VS内置单元测试中,如果您只是想验证是否抛出了“任何异常”,但是您不知道类型,则可以使用全部捕获:

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

3

好吧,我会在这里总结一下其他人之前所说的话。。。无论如何,这是我根据良好答案构建的代码:)剩下要做的就是复制和使用...

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


2

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

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

[ExpectedException(typeof(ArgumentException))]


2

有一个很棒的库叫做NFluent,它可以加快并简化您编写断言的方式

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

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

1

即使这是一个老问题,我还是要在讨论中添加新思想。我已经扩展了“期望,安排,行动,断言”的“安排,行动,断言”模式。您可以创建一个预期的异常指针,然后断言它已分配给它。这比在catch块中执行Asserts感觉更干净,而Act部分仅留给一行代码来调用被测方法。你也不必Assert.Fail();return从代码的多个点。引发的任何其他异常都将导致测试失败,因为它不会被捕获,并且如果引发了预期类型的​​异常,但它不是您所期望的,则针对消息或其他属性断言异常有助于确保您的测试不会意外通过。

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