在 MSTest 单元测试中处理 TPL 异常






4.89/5 (7投票s)
创建 ExpectedAggregateExceptionAttribute,使TPL异常测试更容易。
引言
在过去的几年里,TPL(任务并行库)及其带来的语言特性在 .NET 社区中变得越来越流行。其中一个突出的区别在于异常处理。用户代码在任务中抛出的未处理异常会传播回连接线程。为了将所有异常传播回调用线程,Task 基础设施将它们包装在一个 AggregateException
实例中。
现在这一切都非常清楚,而且非常简单易于管理。我不会详细介绍这个主题,因为您可以在 MSDN 上找到所有必要的信息,网址为 http://msdn.microsoft.com/en-us/library/dd997415(v=vs.110).aspx。
一旦我开始为我的异步方法编写单元测试,我发现我不能再使用我通常的技术了。
背景
为了断言某个单元在特定情况下抛出某个异常,我总是使用 ExpectedException
属性。为了使其更清楚,我将举一个使用示例。
public bool MySimpleMethod(List param)
{
if (param == null)
{
throw new ArgumentNullException("param");
}
return true;
}
我们将编写一个测试,该测试将断言,如果将 null 作为参数传递给此方法调用,则该方法应引发 ArgumentNullException
。
为了做到这一点,请考虑以下代码。
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void MySimpleMethod_Throws_ArgumentNullException()
{
sut.MySimpleMethod(null);
}
一旦执行,此测试将通过。
现在让我们编写前一个方法的异步模拟。
public async Task MySimpleMethodAsync(List param)
{
if (param == null)
{
throw new ArgumentNullException("param");
}
await Task.Delay(100);
return true;
}
由于我们知道我们的异常被包装在 AggregateException
中,因此预计我们的测试将失败。那么我们应该如何测试这种情况呢?使用代码
等等,我真的很喜欢 ExpectedException
属性,我希望有一个属性可以检查任何聚合异常是否是预期类型。在快速搜索之后,我没有找到任何适合我的东西。那是 ExpectedAggregateExceptionAttribute
诞生的时刻。我遵循了 ExpectedException
的模式,这就是我提出的。
///
/// Indicates that an exception is expected during test method execution.
/// It also considers the AggregateException and check if the given exception is contained inside the InnerExceptions.
/// This class cannot be inherited.
///
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public sealed class ExpectedAggregateExceptionAttribute : ExpectedExceptionBaseAttribute
{
protected override string NoExceptionMessage
{
get
{
return string.Format("{0} - {1}, {2}, {3}",
this.TestContext.FullyQualifiedTestClassName,
this.TestContext.TestName,
this.ExceptionType.FullName,
base.NoExceptionMessage);
}
}
///
/// Gets the expected exception type.
///
///
///
/// A object.
///
public Type ExceptionType { get; private set; }
public bool AllowDerivedTypes { get; set; }
///
/// Initializes a new instance of the class with and expected exception type and a message that describes the exception.
///
/// An expected type of exception to be thrown by a method.
public ExpectedAggregateExceptionAttribute(Type exceptionType)
: this(exceptionType, string.Empty)
{
}
///
/// Initializes a new instance of the class with and expected exception type and a message that describes the exception.
///
/// An expected type of exception to be thrown by a method.
/// If the test fails because an exception was not thrown, this message is included in the test result.
public ExpectedAggregateExceptionAttribute(Type exceptionType, string noExceptionMessage)
: base(noExceptionMessage)
{
if (exceptionType == null)
{
throw new ArgumentNullException("exceptionType");
}
if (!typeof(Exception).IsAssignableFrom(exceptionType))
{
throw new ArgumentException("Given type is not an exception.", "exceptionType");
}
this.ExceptionType = exceptionType;
}
/// The exception that is thrown by the unit test.
protected override void Verify(Exception exception)
{
Type type = exception.GetType();
if (this.AllowDerivedTypes)
{
if (!this.ExceptionType.IsAssignableFrom(type))
{
base.RethrowIfAssertException(exception);
throw new Exception(string.Format("Test method {0}.{1} threw exception {2}, but exception {3} was expected. Exception message: {4}",
base.TestContext.FullyQualifiedTestClassName,
base.TestContext.TestName,
type.FullName,
this.ExceptionType.FullName,
GetExceptionMsg(exception)));
}
}
else
{
if (type == typeof(AggregateException))
{
foreach (var e in ((AggregateException)exception).InnerExceptions)
{
if (e.GetType() == this.ExceptionType)
{
return;
}
}
}
if (type != this.ExceptionType)
{
base.RethrowIfAssertException(exception);
throw new Exception(string.Format("Test method {0}.{1} threw exception {2}, but exception {3} was expected. Exception message: {4}",
base.TestContext.FullyQualifiedTestClassName,
base.TestContext.TestName,
type.FullName,
this.ExceptionType.FullName,
GetExceptionMsg(exception)));
}
}
}
private string GetExceptionMsg(Exception ex)
{
StringBuilder stringBuilder = new StringBuilder();
bool flag = true;
for (Exception exception = ex; exception != null; exception = exception.InnerException)
{
string str = exception.Message;
FileNotFoundException notFoundException = exception as FileNotFoundException;
if (notFoundException != null)
{
str = str + notFoundException.FusionLog;
}
stringBuilder.Append(string.Format((IFormatProvider)CultureInfo.CurrentCulture, "{0}{1}: {2}", flag ? (object)string.Empty : (object)" ---> ", (object)exception.GetType(), (object)str));
flag = false;
}
return ((object)stringBuilder).ToString();
}
}
简而言之,我检查抛出的异常是否为 AggregateException
类型,如果是,我再次检查任何内部异常是否为请求的类型。如果未满足此条件,则会抛出新异常,这将使您的单元测试失败。
如果我们现在使用新的属性而不是 ExpectedExceptionAttribute
,则此测试将成功。
[TestMethod]
[ExpectedAggregateException(typeof(ArgumentNullException))]
public void MySimpleMethodAsync_Throws_ArgumentNullException_2()
{
sut.MySimpleMethodAsync(null).Wait();
}
为了确保我们的新属性正常工作,我将进行一些其他测试。首先,我希望单元测试在未抛出任何异常时失败
[TestMethod]
[ExpectedAggregateException(typeof(ArgumentNullException))]
public void ArgumentNullException_Fail_If_No_Exception()
{
sut.MySimpleMethodAsync(new List()).Wait();
}
请注意,此测试并非旨在通过(不要将其包含在您的解决方案中)。此外,如果内部异常中没有预期的异常类型,则测试应该失败
[TestMethod]
[ExpectedAggregateException(typeof(ArgumentNullException))]
public void ArgumentNullException_Fail_If_Wrong_Inner_Exception()
{
Task.Factory.StartNew(() =>
{
throw new ArgumentOutOfRangeException();
});
}
由于这两个测试都失败了,我可以得出结论,这个新属性按预期工作。您可以根据需要将其打包,在您自己的通用库中,在新或现有的自定义测试库中,或在您的任何项目中。
关注点
此属性可以轻松扩展。我已经有一些关于它的想法。例如,考虑一个重载,它将断言给定的异常应该是包装在 AggregateException
中的唯一一个。
如果您有任何想法,请随时评论,并最终在 GitHub 上改进它。
历史
首次发布