Developer Tips: Unit Testing for Exceptions

Our guest blogger, Andrew Hinkle, presents some tips and techniques on how to unit test for exceptions.

Written by Andrew Hinkle • Last Updated: • Develop •
Marionettes causing a problem on a chess board

As I'm creating unit tests for my latest project I found myself yet again dreading the testing of exceptions. The way we test for exceptions just stands out as a sore thumb whenever I look at them.

When I first learned how to unit test exceptions I used the classic ExpectedException attribute. This works fine and is clean as long as you don't care about what message is returned from the exception.

I prefer to verify that the correct exception was thrown with the correct message.

This meant that now I had to use try/catch blocks and add Assert.Fail after the method call for when the exception was not thrown. I've gone through multiple variations and I keep forgetting that the Assert.Fail I added throws an exception itself and is caught by the generic catch for exceptions making the messaging a mess unless I check for the AssertFailedException and rethrow.

I was tired of making this mistake and the repetition of try/catch code that kept changing. I decided to create a TestHelper class to handle this for me.

I created the methods TestForException to take the method calls as a parameter along with the expected exception and let it handle the try/catch block.

To do this I had to create two versions of the method, one for methods with no return type (Action) and another for methods with a return type (Func). I personally prefer the lamda expression inline or assigned to a variable for readability. The local function variation is growing on me though.

Below is the TestHelper class, followed by a simple class with two process methods to demo the Action and Func variations and of course unit tests provided. These code samples and more can be found on my GitHub site.

TestHelper.cs

internal class TestHelper
{
    public static void TestForException(Action method, Exception expectedException)
    {
        // Fail the test if the method does not throw an exception.

        // Example usage:         // Action method = () => _service.ProcessAction(1);         // var expectedException = new ArgumentException("The message.");         // TestHelper.TestForException(method, expectedException);         try         {             method();             Assert.Fail("An exception was not thrown as expected.");         }         catch (Exception e)         {             // If the exception thrown was from the Assert.Fail, then rethrow.             if (e.GetType() == typeof(AssertFailedException)) throw;             Assert.AreEqual(expectedException.GetType(), e.GetType());             Assert.AreEqual(expectedException.Message, e.Message);         }     }
    public static void TestForException<T>(Func<T> method, Exception expectedException)     {         // Fail the test if the method does not throw an exception.
        // Example usage:         // Action method = () => _service.ProcessFunc(1);         // var expectedException = new ArgumentException("The message.");         // TestHelper.TestForException(method, expectedException);         try         {             method();             Assert.Fail("An exception was not thrown as expected.");         }         catch (Exception e)         {             // If the exception thrown was from the Assert.Fail, then rethrow.             if (e.GetType() == typeof(AssertFailedException)) throw;             Assert.AreEqual(expectedException.GetType(), e.GetType());             Assert.AreEqual(expectedException.Message, e.Message);         }     } }

Service.cs

public class Service
{
    public void ProcessAction(int option)
    {
        switch (option)
        {
            case 0:
                throw new SystemException("I will NOT process that.  Eww.");
            case 1:
                throw new ArgumentException("Your connection is cutting out... must be the rain.");
            case 2:
                throw new ArgumentNullException(nameof(option));
            case 3:
                throw new ApplicationException("Nope, not going to let you do that.");
            default:
                DoWork();
                break;
        }
    }
    public bool ProcessFunc(int option)
    {
        switch (option)
        {
            case 0:
                throw new SystemException("I've got a paperclip and bubblegum, we'll fix this.");
            case 1:
                throw new ArgumentException("What were you thinking?");
            case 2:
                throw new ArgumentNullException(nameof(option));
            case 3:
                throw new ApplicationException("That's just wrong.  Stop.  Just stop.");
            default:
                DoWork();
                break;
        }
        return true;
    }
    private void DoWork()
    {
        // Perform actions.
    }
}

ServiceProcessActionTest.cs

[TestClass]
public class ServiceProcessActionTest
{
    private readonly Service _service;
    public ServiceProcessActionTest()
    {
        _service = new Service();
    }
    [TestMethod]
    public void WhenOption0ThenException_OldWay()
    {
        var expectedException = new SystemException("I will NOT process that.  Eww."); ;
        try
        {
            _service.ProcessAction(0);
            Assert.Fail("An exception was not thrown as expected.");
        }
        catch (Exception e)
        {
            // If the exception thrown was from the Assert.Fail, then rethrow.
            if (e.GetType() == typeof(AssertFailedException)) throw;
            Assert.AreEqual(expectedException.GetType(), e.GetType());
            Assert.AreEqual(expectedException.Message, e.Message);
        }
    }
    [TestMethod]
    public void WhenOption0ThenException()
    {
        // Anonymous method
        // https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/anonymous-methods
        Action method = delegate { _service.ProcessAction(0); };
        var expectedException = new SystemException("I will NOT process that.  Eww.");
        TestHelper.TestForException(method, expectedException);
    }
    [TestMethod]
    public void WhenOption1ThenException()
    {
        // Expression lambda
        // https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/lambda-expressions
        Action method = () => _service.ProcessAction(1);
        var expectedException = new ArgumentException("Your connection is cutting out... must be the rain.");
        TestHelper.TestForException(method, expectedException);
    }
    [TestMethod]
    public void WhenOption2ThenException()
    {
        // Local function
        // https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/local-functions
        void Method() => _service.ProcessAction(2);
        var expectedException = new ArgumentNullException($"option");
        TestHelper.TestForException(Method, expectedException);
    }
    [TestMethod]
    public void WhenOption3ThenException()
    {
        // Inline Lamda
        TestHelper.TestForException(() => _service.ProcessAction(3), new ApplicationException("Nope, not going to let you do that."));
    }
    [TestMethod]
    public void WhenOption3ThenNoException()
    {
        _service.ProcessAction(4);
    }
}

ServiceProcessFuncTest.cs

[TestClass]
public class ServiceProcessFuncTest
{
    private readonly Service _service;
    public ServiceProcessFuncTest()
    {
        _service = new Service();
    }
    [TestMethod]
    public void WhenOption0ThenException_OldWay()
    {
        var expectedException = new SystemException("I've got a paperclip and bubblegum, we'll fix this.");;
        try
        {
            _service.ProcessFunc(0);
            Assert.Fail("An exception was not thrown as expected.");
        }
        catch (Exception e)
        {
            // If the exception thrown was from the Assert.Fail, then rethrow.
            if (e.GetType() == typeof(AssertFailedException)) throw;
            Assert.AreEqual(expectedException.GetType(), e.GetType());
            Assert.AreEqual(expectedException.Message, e.Message);
        }
    }
    [TestMethod]
    public void WhenOption0ThenException()
    {
        // Anonymous method
        // https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/anonymous-methods
        Action method = delegate { _service.ProcessFunc(0); };
        var expectedException = new SystemException("I've got a paperclip and bubblegum, we'll fix this.");
        TestHelper.TestForException(method, expectedException);
    }
    [TestMethod]
    public void WhenOption1ThenException()
    {
        // Expression lambda
        // https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/lambda-expressions
        Action method = () => _service.ProcessFunc(1);
        var expectedException = new ArgumentException("What were you thinking?");
        TestHelper.TestForException(method, expectedException);
    }
    [TestMethod]
    public void WhenOption2ThenException()
    {
        // Local function
        // https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/local-functions
        void Method() => _service.ProcessFunc(2);
        var expectedException = new ArgumentNullException($"option");
        TestHelper.TestForException(Method, expectedException);
    }
    [TestMethod]
    public void WhenOption3ThenException()
    {
        // Inline Lamda
        TestHelper.TestForException(() => _service.ProcessFunc(3), new ApplicationException("That's just wrong.  Stop.  Just stop."));
    }
    [TestMethod]
    public void WhenOption3ThenNoException()
    {
        Assert.IsTrue(_service.ProcessFunc(4));
    }
}

Conclusion

With these test for exception helper methods I no longer cringe when I need to add unit tests for exceptions. My unit tests for exceptions are now shorter, cleaner, easier to read, and easier to maintain.

How do you create your unit tests for testing for exceptions? Which way do you prefer? Do you have a better way? Let us know in the comments below!

ASP.NET 8 Best Practices on Amazon

ASP.NET 8 Best Practices by Jonathan Danylko


Reviewed as a "comprehensive guide" and a "roadmap to excellence" with over 120 Best Practices for ASP.NET Core 8, Jonathan's first book by Packt Publishing explores proven techniques for every phase of the SDLC.

Learn industry-standard concepts to improve your coding, debugging, and deployment of ASP.NET Core websites.

Order now on Amazon.com button

Picture of Andrew Hinkle

Andrew Hinkle has been developing applications since 2000 from LAMP to Full-Stack ASP.NET C# environments from manufacturing, e-commerce, to insurance.

He has a knack for breaking applications, fixing them, and then documenting it. He fancies himself as a mentor in training. His interests include coding, gaming, and writing. Mostly in that order.

comments powered by Disqus