The ultimate ExceptionAssert.Throws() method

A while ago I published a post titled Pimp your VSTT exception tests in which I pointed out some disadvantages of VSTT's declarative approach to testing for exceptions and provided an alternative. I recently gave this some more thought while writing some new tests and noticed that my original version could be significantly improved by a simple tweak leading to the ultimate version of the Throws() methods (still, don't be too surprised if I publish a post titled The ultimate ExceptionAssert .Throws() method v2.0 at some point).

First of all, when writing my original post it seemed like having overloads would be a good idea because of how anonymous methods are treated by the compiler and the potential side-effects. Looking back at it I'd say that was an overly cautious approach. In the tests I've written since I published the post there wasn't any instance where accessing fields and locals would cause problems. The second point is the really interesting one though. Consider the following code:

public static void Foo(int i)

{

    if (i < 0)

        throw new ArgumentOutOfRangeException(

            "i",

            i,

            "The value of parameter 'i' must not be less than zero.");

}

 

When testing whether or not the method throws as expected, the test code will often just check if an exception of the correct type was thrown. If the code is part of an application and only used internally this may actually be enough and just checking the type is somewhat encouraged by how it is achieved in VSTT with the ExpectedExceptionAttribute. The same is true for my original Throws() method. But if you're building components for others to use in their applications you certainly want to make sure that the exception object looks exactly the way it was specified.

In the sample above you would want to verify the properties ActualValue, Message and ParamName. But generally speaking the approach to exception testing used when creating libraries must allow the tester to perform arbitrary validation on the exception object. While Throws() could for example simply return the exception it caught - probably the simplest solution - this would be not very consistent with how a typical assert method works. It is void-returning and only returns if the assert passed. A slightly more difficult approach which is equally flexible in this context leads to more consistent test methods. Since we're already dealing with delegates for telling Throws() what code exactly should throw, why not pass in a second delegate which verifies the exception object using standard assert methods so that a test for the method above would look like this:

[TestMethod]

public void TestMethod()

{

    ExceptionAssert.Throws(

        () => MyClass.Foo(Int32.MinValue),

        (ArgumentOutOfRangeException e) =>

        {

            Assert.AreEqual("i", e.ParamName);

            Assert.AreEqual(Int32.MinValue, e.ActualValue);

            Assert.IsTrue(e.Message.StartsWith("The value of parameter 'i' must not be less than zero."));

        });

}

 

The biggest disadvantage is that test methods will probably rely on anonymous methods in many cases to keep the code compact and make it easy to follow the control flow but it can take a while get used to reading/writing this kind of code. Still, if you want/need to go deeper than just looking at exception types I'd say that this is a good way of doing it and - unless I've overlooked something (again) - the only Throws() method you'll ever need (especially since the second parameter is optional).

public static class ExceptionAssert

{

    // The type parameter is used for passing in the expected exception type.

    // The where clause is used to ensure that T is Exception or a subclass.

    // Thus an explicit check is not necessary.

    public static void Throws<T>(Action action, Action<T> validator) where T : Exception

    {

        if (action == null)

            throw new ArgumentNullException("action");

       // Executing the action in a try block since we expect it to throw.

        try

        {

            action();

        }

        // Catching the exception regardless of its type.

        catch (Exception e)

        {

            // Comparing the type of the exception to the type of the type

            // parameter, failing the assert in case they are different,

            // even if the type of the exception is derived from the expected type.

            if (e.GetType() != typeof(T))

                throw new AssertFailedException(String.Format(

                    CultureInfo.CurrentCulture,

                    "ExceptionAssert.Throws failed. Expected exception type: {0}. Actual exception type: {1}. Exception message: {2}",

                    typeof(T).FullName,

                    e.GetType().FullName,

                    e.Message));

            // Calling the validator for the exception object if one was

            // provided by the caller. The validator is expected to use the

            // Assert class for performing the verification.

            if (validator != null)

                validator((T)e);

            // Type check passed and validator did not throw.

            // Everything is fine.

            return;

        }

        // Failing the assert since there was no exception.

        throw new AssertFailedException(String.Format(

            CultureInfo.CurrentCulture,

            "ExceptionAssert.Throws failed. No exception was thrown (expected exception type: {0}).",

            typeof(T).FullName));

    }

}


This posting is provided "AS IS" with no warranties, and confers no rights.