MSTest: ExpectedExceptionWithMessageAttribute

Mariusz Wojcik

The unit testing system which comes with Visual Studio offers a way to assert whether a test has thrown specific exception. It is done by using ExpectedExceptionAttribute on test method:

[TestMethod]
[ExpectedException(typeof(InvalidOperationException))]
public void ThrowsInvalidOperationException()
{
    throw new InvalidOperationException();
}

The problem with this solution is that it checks only for the type of the exception, but there is no way to assert exception’s message (There is overloaded constructor which takes string as a second parameter, but this string is not a message to assert, but it is a message which will be displayed when assertion fails.). As exceptions are an important part of application’s domain, our unit tests should assert them and check whether messages are containing useful and valid information. Below example, which is very simplistic, shows how one exception type may reference to two different conditions:

public void ProcessNameAndDescription(string name, string description)
{
    if (name == null)
        throw new ArgumentNullException("name", "Parameter \"name\" may not be null.");
 
    if (description == null)
        throw new ArgumentNullException("description", "Parameter \"description\" may not be null.");
 
    // rest of the code
}

In the above example, the ArgumentNullAttribute can be thrown because of two reasons: either the name or description parameter is null. The tests could be as follow:

[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void ThrowsArgumentNullExceptionWhenProcessingNullName()
{
    ProcessNameAndDescription(null, "some description");
}
 
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void ThrowsArgumentNullExceptionWhenProcessingNullDescription()
{
    ProcessNameAndDescription("some name", null);
}

That works, although it is really easy to make a mistake and assert wrong condition (especially when you use copy pasting for such methods), and without asserting exception’s message there is no way to discover which parameter was wrong.

ExpectedExceptionWithMessageAttribute

Fortunately the unit testing framework can be easily extended and we can create our own attribute which will assert for exception and its message. All what has to be done is to create a new attribute class which inherits from ExpectedExceptionBaseAttribute and provides the implementation of Verify abstract method. When tested code throws an exception, this exception is passed to Verify method and can be checked whether it is what should be expected or not. Below is a source code for sample implementation:

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public sealed class ExpectedExceptionWithMessageAttribute : ExpectedExceptionBaseAttribute
{
    #region private members
 
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private readonly Type _exceptionType;
 
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private readonly string _expectedMessagePattern;
 
    #endregion
 
    #region public properties
 
    public Type ExceptionType
    {
        get { return this._exceptionType; }
    }
 
    public string ExpectedMessagePattern
    {
        get { return this._expectedMessagePattern; }
    }
 
    public bool AllowDerivedTypes
    {
        get;
        set;
    }
 
    #endregion
 
    #region ctor()
 
    public ExpectedExceptionWithMessageAttribute(Type exceptionType, string expectedMessage)
        : this(exceptionType, expectedMessage, String.Empty)
    {
    }
 
    public ExpectedExceptionWithMessageAttribute(Type exceptionType, string expectedMessage, string noExceptionMessage)
        : base(noExceptionMessage)
    {
        #region preconditions
 
        if (exceptionType == null)
            throw new ArgumentNullException("exceptionType", "Parameter \"exceptionType\" may not be null.");
 
        if (!typeof(Exception).IsAssignableFrom(exceptionType))
            throw new ArgumentException("The expected exception type must be System.Exception or a type derived from System.Exception.", "exceptionType");
 
        if (expectedMessage == null)
            throw new ArgumentNullException("expectedMessage", "Parameter \"expectedMessage\" may not be null.");
 
        #endregion
 
        this._exceptionType = exceptionType;
        this._expectedMessagePattern = expectedMessage;
    }
 
    #endregion
 
    protected override void Verify(Exception exception)
    {
        Type exceptionType = exception.GetType();
 
        if (this.AllowDerivedTypes)
        {
            if (!this.ExceptionType.IsAssignableFrom(exceptionType))
            {
                base.RethrowIfAssertException(exception);
                this.HandleInvalidExpectedExceptionOrMessage("Test method {0}.{1} threw exception {2}, but exception {3} or a type derived from it was expected.\r\nException message: {4}.", exception);
            }
            else if (!Regex.IsMatch(exception.Message, this.ExpectedMessagePattern))
            {
                this.HandleInvalidExpectedExceptionOrMessage("Test method {0}.{1} threw expected exception {2} with message \"{4}\" but message with pattern \"{5}\" was expected.", exception);
            }
        }
        else
        {
            if (exceptionType != this.ExceptionType)
            {
                base.RethrowIfAssertException(exception);
                this.HandleInvalidExpectedExceptionOrMessage("Test method {0}.{1} threw exception {2}, but exception {3} was expected.\r\nException message: {4}.", exception);
            }
            else if (!Regex.IsMatch(exception.Message, this.ExpectedMessagePattern))
            {
                this.HandleInvalidExpectedExceptionOrMessage("Test method {0}.{1} threw expected exception {2} with message \"{4}\" but message with pattern \"{5}\" was expected.", exception);
            }
        }
    }
 
    private void HandleInvalidExpectedExceptionOrMessage(string messageTemplate, Exception exceptionThrow)
    {
        throw new Exception(String.Format(messageTemplate,
                    base.TestContext.FullyQualifiedTestClassName,
                    base.TestContext.TestName,
                    exceptionThrow.GetType().FullName,
                    this.ExceptionType.FullName,
                    exceptionThrow.Message,
                    this.ExpectedMessagePattern));
    }
}

Now, we can assert exception for a message pattern (using regular expressions) and our tests may be changed to following:

[TestMethod]
[ExpectedExceptionWithMessage(typeof(ArgumentNullException), "Parameter \"name\" may not be null.")]
public void ThrowsArgumentNullExceptionWhenProcessingNullName()
{
    ProcessNameAndDescription(null, "some description");
}
 
[TestMethod]
[ExpectedExceptionWithMessage(typeof(ArgumentNullException), "Parameter \"description\".*")]
public void ThrowsArgumentNullExceptionWhenProcessingNullDescription()
{
    ProcessNameAndDescription("some name", null);
}

Final note

One final note on asserting exceptions. I do not recommend using this approach for every exception and write your code in a way that only exception’s message distinguishes between different types of errors. The application should be designed in a way, that all domain specific exceptions are represented with their own exception types, such as: ClientAccountLockedOut, DeliveryAddressUnrecognised, etc. so we can assert for those exceptions and assert exception’s message to confirm that it contains valuable information (an Id of client’s account, or description why the address has not been recognised).