如何使用Assert
(或其他 Test 类)来验证是否引发了异常?
24 回答
对于“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");
}
通常你的测试框架会对此给出答案。但如果它不够灵活,你可以随时这样做:
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
}
如果您绝对必须捕获异常,则需要重新抛出 Assert.Fail()。但实际上,这表明你不应该手写这个;检查你的测试框架的选项,或者看看你是否可以抛出一个更有意义的异常来测试。
catch (AssertionException) { throw; }
您应该能够根据自己的喜好调整这种方法——包括指定要捕获的异常类型。如果您只期望某些类型,请使用以下命令完成catch
块:
} catch (GoodException) {
} catch (Exception) {
// not the right kind of exception
Assert.Fail();
}
我首选的实现方法是编写一个名为 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());
}
它的外观和行为更像您的单元测试语法的其余部分。
如果你使用 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
如果您使用的是最初没有ExpectedException
属性的 MSTest,您可以这样做:
try
{
SomeExceptionThrowingMethod()
Assert.Fail("no exception thrown");
}
catch (Exception ex)
{
Assert.IsTrue(ex is SpecificExceptionType);
}
MSTest (v2) 现在有一个 Assert.ThrowsException 函数,可以像这样使用:
Assert.ThrowsException<System.FormatException>(() =>
{
Story actual = PersonalSite.Services.Content.ExtractHeader(String.Empty);
});
您可以使用 nuget 安装它:Install-Package MSTest.TestFramework
小心使用 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/
在我正在做的一个项目中,我们有另一个解决方案。
首先,我不喜欢 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;
}
整洁,不是吗;)
您可以通过简单的一行来实现这一点。
如果您的操作foo.bar()
是异步的:
await Assert.ThrowsExceptionAsync<Exception>(() => foo.bar());
如果foo.bar()
不是异步的
Assert.ThrowsException<Exception>(() => foo.bar());
它是测试方法的一个属性......你不使用 Assert。看起来像这样:
[ExpectedException(typeof(ExceptionType))]
public void YourMethod_should_throw_exception()
您可以使用以下方法从 Nuget 下载包:PM> Install-Package MSTestExtensions,它将nUnit/xUnit 样式的Assert.Throws()语法添加到 MsTest。
高级说明:下载程序集并从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
在 VS 内置单元测试中,如果您只是想验证是否抛出了“任何异常”,但您不知道类型,您可以使用 catch all:
[TestMethod]
[ExpectedException(typeof(Exception), AllowDerivedTypes = true)]
public void ThrowExceptionTest()
{
//...
}
我不建议使用 ExpectedException 属性(因为它太受约束且容易出错)或在每个测试中编写 try/catch 块(因为它太复杂且容易出错)。使用设计良好的断言方法——要么由您的测试框架提供,要么自己编写。这是我编写和使用的内容。
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)
}
笔记
返回异常而不是支持验证回调是一个合理的想法,只是这样做会使这个断言的调用语法与我使用的其他断言非常不同。
与其他人不同,我使用“传播”而不是“抛出”,因为我们只能测试异常是否从调用传播。我们无法直接测试是否引发了异常。但我想你可以想象 throws 的意思是:被抛出而不是被抓住。
最后的想法
在切换到这种方法之前,我考虑在测试仅验证异常类型时使用 ExpectedException 属性,如果需要更多验证,则使用 try/catch 块。但是,我不仅要考虑每次测试使用哪种技术,而且随着需求的变化将代码从一种技术更改为另一种技术也不是一件容易的事。使用一种一致的方法可以节省脑力。
所以总而言之,这种方法具有:易用性、灵活性和鲁棒性(很难做错)。
好吧,我会总结一下这里其他人之前所说的话......无论如何,这是我根据好的答案构建的代码:) 剩下要做的就是复制和使用......
/// <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.");
}
上面@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))
);
}
}
}
}
由于您提到使用其他测试类,比ExpectedException
属性更好的选择是使用Shoudly的Should.Throw。
Should.Throw<DivideByZeroException>(() => { MyDivideMethod(1, 0); });
假设我们要求客户必须有一个地址才能创建订单。如果不是,该CreateOrderForCustomer
方法应该导致ArgumentException
. 然后我们可以写:
[TestMethod]
public void NullUserIdInConstructor()
{
var customer = new Customer(name := "Justin", address := null};
Should.Throw<ArgumentException>(() => {
var order = CreateOrderForCustomer(customer) });
}
这比使用ExpectedException
属性要好,因为我们要明确什么应该引发错误。这使我们测试中的要求更加清晰,并且在测试失败时也使诊断更加容易。
注意还有一个Should.ThrowAsync
用于异步方法测试。
作为替代方案,您可以尝试测试异常实际上是在测试中的下两行引发的。
var testDelegate = () => MyService.Method(params);
Assert.Throws<Exception>(testDelegate);
有一个很棒的库,叫做NFluent,它可以加快并简化您编写断言的方式。
编写一个抛出异常的断言非常简单:
[Test]
public void given_when_then()
{
Check.ThatCode(() => MethodToTest())
.Throws<Exception>()
.WithMessage("Process has been failed");
}
这将取决于您使用的是什么测试框架?
例如,在 MbUnit 中,您可以使用属性指定预期的异常,以确保获得您真正期望的异常。
[ExpectedException(typeof(ArgumentException))]
查看nUnit Docs以获取有关以下内容的示例:
[ExpectedException( typeof( ArgumentException ) )]
如果使用NUnit,试试这个:
Assert.That(() =>
{
Your_Method_To_Test();
}, Throws.TypeOf<Your_Specific_Exception>().With.Message.EqualTo("Your_Specific_Message"));
我知道这个线程很旧并且有很多很好的答案,但也许值得一提的是本地函数可以以非常简单的方式提供帮助。
//Arrange
//Act
void LocalFunction() => mr.ActualMethod(params);
//Assert
Assert.Throws<Exception>(LocalFunction);
尽管这是一个老问题,但我想在讨论中添加一个新想法。我已经将 Arrange, Act, Assert 模式扩展为 Expected, Arrange, Act, Assert。您可以创建一个预期的异常指针,然后断言它被分配给。这感觉比在 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);
}
这适用于
处理数据库或 http 事务时的Visual Studio Team Test(又名MSTest) 。
系统应该在某处抛出异常,使用Assert.ThrowExceptionAsync<>()将捕获您的 Throw 事件。(在这些情况下,Assert.ThrowException<>()不会捕获异常)。
[TestMethod]
public void Invalid_Input_UserName_Should_Throw_Exception()
{
await Assert.ThrowExceptionAsync<ExpectedExceptionType>(()=> new LogonInfo(InvalidInputInUserNameFormat,"P@ssword"));
}