65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (7投票s)

2014年10月2日

CPOL

3分钟阅读

viewsIcon

23893

downloadIcon

87

创建 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 上改进它。

历史

首次发布

© . All rights reserved.