Click here to Skip to main content
15,885,366 members
Articles / Programming Languages / C# 5.0

Handling TPL exceptions in MSTest unit tests

Rate me:
Please Sign up or sign in to vote.
4.89/5 (7 votes)
2 Oct 2014CPOL3 min read 22.9K   62   6   3
Creating ExpectedAggregateExceptionAttribute thatmakes TPL exceptions testing easier.

Introduction

In the past couple of years TPL (Task Parallel Library) and language features it brought became more and more popular in the .NET community. One of the differences that stood up is the exception handling. Unhandled exceptions that are thrown by user code that is running inside a task are propagated back to the joining thread. To propagate all the exceptions back to the calling thread, the Task infrastructure wraps them in an AggregateException instance.

Now this is all clear and quite straight forward to manage. I will not go in the detail about this topic as you can find all of the necessary information on MSDN at http://msdn.microsoft.com/en-us/library/dd997415(v=vs.110).aspx.

Once I started writing unit tests for my async methods I found out that I can’t anymore use my usual technique.

Background

In order to assert that a certain unit is throwing a certain exception in well determined circumstances, I always went for the ExpectedException attribute. To make it even clearer, I will make an example of usage.

Now imagine the following class:
C#
public bool MySimpleMethod(List<string> param)
{
    if (param == null)
    {
        throw new ArgumentNullException("param");
    }
 
    return true;
}

We are going to write a test that will assert that in case of a null being passed as a parameter to this method call, the method should raise an ArgumentNullException.

In order to do so, consider the following code.

C#
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void MySimpleMethod_Throws_ArgumentNullException()
{
    sut.MySimpleMethod(null);
}

Once executed, this test will pass.

Now let’s write an async analog of the previous method.

C#
public async Task<bool> MySimpleMethodAsync(List<string> param)
{
    if (param == null)
    {
        throw new ArgumentNullException("param");
    }
 
    await Task.Delay(100);
 
    return true;
}
As we do know that our exception is wrapped in an AggregateException, it is expected our test to fail. How should we than test this kind of situations?

Using the code

Wait, I really liked the ExpectedException attribute, I would like to have an attribute that will check if any of the aggregated exceptions are of the type I do expect. After a quick search I hadn't found anything that suites me. That was the moment the ExpectedAggregateExceptionAttribute was born. I followed the pattern on which ExpectedException was made and this is what I came up with.

C#
/// <summary>
/// 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.
/// </summary>
[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);
        }
    }
 
    /// <summary>
    /// Gets the expected exception type.
    /// </summary>
    /// 
    /// <returns>
    /// A <see cref="T:System.Type"/> object.
    /// </returns>
    public Type ExceptionType { get; private set; }
 
    public bool AllowDerivedTypes { get; set; }
 
    /// <summary>
    /// Initializes a new instance of the <see cref="ExpectedAggregateExceptionAttribute"/> class with and expected exception type and a message that describes the exception.
    /// </summary>
    /// <param name="exceptionType">An expected type of exception to be thrown by a method.</param>
    public ExpectedAggregateExceptionAttribute(Type exceptionType)
        : this(exceptionType, string.Empty)
    {
    }
 
    /// <summary>
    /// Initializes a new instance of the <see cref="ExpectedAggregateExceptionAttribute"/> class with and expected exception type and a message that describes the exception.
    /// </summary>
    /// <param name="exceptionType">An expected type of exception to be thrown by a method.</param>
    /// <param name="noExceptionMessage">If the test fails because an exception was not thrown, this message is included in the test result.</param>
    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;
    }
 
    /// <param name="exception">The exception that is thrown by the unit test.</param>
    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();
    }
}

To cut it short, I do check if the thrown exception is of AggregateException type, and if so I do again check if any of the inner exceptions is of the requested type. In case this condition is not matched a new exception is thrown which will make your unit test fail.

If we now use our new attribute instead of the ExpectedExceptionAttribute this test will be successful.

C#
[TestMethod]
[ExpectedAggregateException(typeof(ArgumentNullException))]
public void MySimpleMethodAsync_Throws_ArgumentNullException_2()
{
    sut.MySimpleMethodAsync(null).Wait();
}

In order to be sure that our new attribute is working properly I will make some other tests. First I do expect the unit test to fail in case no exception was thrown:

C#
[TestMethod]
[ExpectedAggregateException(typeof(ArgumentNullException))]
public void ArgumentNullException_Fail_If_No_Exception()
{
    sut.MySimpleMethodAsync(new List<string>()).Wait();
}

Mind that this test is not meant to pass (do not include it in your solution). Also, if in the inner exceptions there is no expected exception type, the test should fail:

C#
[TestMethod]
[ExpectedAggregateException(typeof(ArgumentNullException))]
public void ArgumentNullException_Fail_If_Wrong_Inner_Exception()
{
    Task.Factory.StartNew(() =>
    {
        throw new ArgumentOutOfRangeException();
    });
}

As both test failed, I can conclude this new attribute works as supposed. You can package it as you wish, in your own common library, in a new or existing custom testing library or in any of your projects.

Points of Interest

This attribute can be expanded easily. I already do have some ideas about it. At example consider an overload that will assert that the given exception should be the only one wrapped in the AggregateException.

If you have any ideas, feel free to comment and eventually improve it on GitHub.

History

Initial release

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
Netherlands Netherlands
An accomplished software engineer specialized in object-oriented design and analysis on Microsoft .NET platform with extensive experience in the full life cycle of the software design process.
Experienced in agile software development via scrum and kanban frameworks supported by the TFS ALM environment and JIRA. In depth know how on all automation process leading to continuous integration, deployment and feedback.
Additionally, I have a strong hands-on experience on deploying and administering Microsoft Team Foundation Server (migrations, builds, deployment, branching strategies, etc.).

Comments and Discussions

 
QuestionTestContext references in Attribute Pin
Jamie Clayton26-Oct-17 16:46
Jamie Clayton26-Oct-17 16:46 
QuestionGreat Handling TPL exceptions Code Pin
Olivia Rousseff30-Jul-15 20:13
Olivia Rousseff30-Jul-15 20:13 
GeneralMy Vote 5 Pin
Shemeemsha (ഷെമീംഷ)2-Oct-14 5:36
Shemeemsha (ഷെമീംഷ)2-Oct-14 5:36 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.