Unit Testing Exceptions in C#

by Chad
Published January 16, 2020
Last updated November 10, 2024

Waves Waves

Sometimes there are cases where we want to throw a specific exception in our code. When you are writing your tests, how do you account for this? In this article I will work through examples of how to unit test C# code that's expected to throw exceptions.

Testing Series

I plan on making this article just one of many articles that are all to do with testing your C#/.NET code. This article is the second in the series. My previous article was an introduction to unit testing C#/.NET code with the help of the xUnit.NET testing library. Here is the C#/.NET testing series thus far.

  1. Unit Testing Your C# Code with xUnit
  2. Unit Testing Exceptions in C#

For this article, I will start with the code I wrote in my previous article. If you'd like to see that code, I've posted it on my Github, and you can see it here. If you see something wrong or something that could be improved, feel free to submit a pull request! In that article, I wrote a SpeedConversionService whose sole purpose was to convert incoming speed units expressed as kilometers per hour into miles per hour. Using a test driven development (TDD) Red-Green-Refactor approach with the help of xUnit, I did not touch the codebase without first writing a failing test. For this article, I will continue using a TDD approach.

The Code for this Article

If you would like to see the full source, including all the code and the test project, I've posted the code specifically for testing exceptions on my GitHub. As always, if you have a suggestion or feel you could make it better, feel free to submit a pull request!

Test for Exceptions using xUnit's Assert.Throws<T>

xUnit kindly provides a nice way of capturing exceptions within our tests with Assert.Throws<T>. All we need to do is supply Assert.Throws<T> with an exception type, and an Action that is supposed to throw an exception. Since we're following Red-Green-Refactor, we're going to start with a failing test. We're going to test the case where we call SpeedConversionService's ConvertToMilesPerHour method and pass it -1 as the inputted kilometers per hour.

Since speed, in the math and physics world, is considered a scalar quantity with no representation of direction, a "negative" speed isn't possible. In our code, we need to add a rule where we cannot convert negative values of kilometers per hour. We want to throw an exception, specifically an ArgumentOutOfRangeException, if the ConvertToMilesPerHour method is passed a negative input. Here's how we'll do it with xUnit.

[Fact]
public void ConvertToMilesPerHour_InputNegative1_ThrowsArgumentOutOfRangeException()
{
    Assert.Throws<ArgumentOutOfRangeException>(() => speedConverter.ConvertToMilesPerHour(-1));
}

First, we decorated the test method with [Fact]. [Fact], as I mentioned in my previous article on unit testing C#/.NET code with xUnit, is used to tell the test runner to actually run the test. If the test runner completes the test without throwing an exception or failing an Assert, the test passes.

Next, we provide the type argument, which needs to be a type of Exception, the type of exception we expect our code to throw, ArgumentOutOfRangeException.

Finally, we pass in an Action as an empty lambda expression that simply calls our class under test SpeedConversionService's ConvertToMilesPerHour method with -1 as the input parameter.

If we run our test, it fails. Since we're following TDD, we'll easily start with a failing test since we don't have any such code that throws an ArgumentOutOfRangeException. Here's the output from my Visual Studio 2019 Test Explorer.

Visual Studio 2019 shows a failing test

Now, we need to write the code to make our test pass.

public int ConvertToMilesPerHour(int kilometersPerHour)
{
    if (kilometersPerHour == -1)
    {
        throw new ArgumentOutOfRangeException($"{nameof(kilometersPerHour)} must be positive.");
    }

    return (int)Math.Round(kilometersPerHour * 0.62137);
}

All I've done is added a new guard clause that checks if kilometersPerHour is -1. If it is, it will throw the exception. Our code should now pass the test because we throw the expected ArgumentOutOfRangeException. I've also used C#'s string interpolation and the nameof operator to specify the exception message. The nameof operator will simply enforce the name of kilometersPerHour is consistent with what we place in the exception message via compilation.

Visual Studio 2019 Test Explorer now shows all tests are passing

Great! Our test is now passing, but we still have a problem. What if we input -2? Let's write a test.

[Theory]
[InlineData(-1)]
[InlineData(-2)]
public void ConvertToMilesPerHour_InputNegative_ThrowsArgumentOutOfRangeException(int input)
{
    Assert.Throws(() => speedConverter.ConvertToMilesPerHour(input));
}

I've changed our test to use the [Theory] and [InlineData] attributes instead of the [Fact] attribute. As I demonstrated in my previous article, this will allow us to write less code to perform more tests. Now, let's see what happens when we run all of the tests.

Visual Studio 2019 Test Explorer again shows a failing test

We need to modify the code to throw an ArgumentOutOfRangeException for all negative input.

if (kilometersPerHour <= -1)
{
    throw new ArgumentOutOfRangeException($"{nameof(kilometersPerHour)} must be positive.");
}

The tests are all passing once again in Visual Studio 2019 Test Explorer

Asserting Exception Messages

So far so good, our code now throws an ArgumentOutOfRangeException when inputting a negative integer, and our tests cover that. But what would we do if we added more requirements to our code, and it could throw ArgumentOutOfRangeExceptions for different reasons? For this, we can actually ensure we've thrown the correct exception by inspecting the exception message of the return value of Assert.Throws<T>. A neat feature of Assert.Throws<T> is that it actually returns the exception that was thrown within the passed Action.

Let's say we want our current ArgumentOutOfRangeException's to throw with the exception message: "The input kilometersPerHour must be greater than or equal to zero." We'll need to modify our tests to account for this.

[Theory]
[InlineData(-1)]
[InlineData(-2)]
public void ConvertToMilesPerHour_InputNegative_ThrowsArgumentOutOfRangeException(int input)
{
    var ex = Assert.Throws<ArgumentOutOfRangeException>(() => speedConverter.ConvertToMilesPerHour(input));

    Assert.Contains("must be greater than or equal to zero.", ex.Message);
}

I've changed the test method to store the result of Assert.Throws<T> into a variable, ex. Then I use Assert.Contains to ensure my ex, the ArgumentOutOfRangeException thrown by my code, contains the string "must be greater than or equal to zero." I could have used Assert.Equals here to ensure they exactly match, but I decided to use Assert.Contains in case I wanted to change the first part of the exception message in the name of easier maintenance.

Our tests are now going to fail since the exception doesn't match what we're expecting in the test.

Visual Studio 2019 Test Explorer shows our tests are failing

Now that we have our failing tests, let's write the code needed to make the tests pass. We'll need to change the exception message when we throw the ArgumentOutOfRangeException in our code.

if (kilometersPerHour <= -1)
{
    throw new ArgumentOutOfRangeException($"{nameof(kilometersPerHour)} must be greater than or equal to zero.");
}

And once again, our tests all pass!

All of the tests are passing within Visual Studio 2019 Text Explorer

Testing Exceptions Regardless of Test Framework

Okay, so testing for the exceptions our code throws is great and all, but what if we don't use xUnit in our test project? We can test our exceptions using any testing framework such as MSTest, a still-popular testing framework developed by Microsoft, or NUnit, another wildly popular testing framework for .NET applications.

The way to do this is using good ole' fashioned C# try/catch blocks. Like xUnit's way of testing exceptions with Assert.Throws<T>, it's simple to test exceptions, but we must be mindful of the flow of the try/catch logic within our test methods.

If we wanted to ensure that our code simply throws the ArgumentOutOfRangeException given a negative input, we'd write our test like this.

[Theory]
[InlineData(-1)]
[InlineData(-2)]
public void FrameworkAgnostic_ConvertToMilesPerHour_InputNegative_ThrowsArgumentOutOfRangeException(int input)
{
    try
    {
        speedConverter.ConvertToMilesPerHour(input);
    }
    catch (ArgumentOutOfRangeException)
    {
        return;
    }

    throw new InvalidOperationException($"Expected {nameof(ArgumentOutOfRangeException)} but no exception was thrown.");
}

While I used the [Theory] and [InlineData] attributes which are specific to xUnit, you could use attributes from whichever flavor of testing framework you choose such as [Test] or [TestMethod]. I've wrapped my call to ConvertToMilesPerHour within a try block to give our test method a chance to catch the exception within the catch block. If we arrive at the catch block, which indicates our code threw the ArgumentOutOfRangeException as expected, we can simply return, as our work is done here, and our test will pass. Otherwise, if our code continues executing after the call to ConvertToMilesPerHour, we know our code didn't throw the ArgumentOutOfRangeException as expected, thus I throw an InvalidOperationException here with an appropriate message to signal that something went wrong, and the test will fail.

Similarly, if we wanted to check for a specific message after the exception is thrown, we need to modify the catch block to inspect the exception message.

[Theory]
[InlineData(-1)]
[InlineData(-2)]
public void FrameworkAgnostic_ConvertToMilesPerHour_InputNegative_ThrowsArgumentOutOfRangeException(int input)
{
    try
    {
        speedConverter.ConvertToMilesPerHour(input);
    }
    catch (ArgumentOutOfRangeException ex)
    {
        Assert.Contains("must be greater than or equal to zero.", ex.Message);
        return;
    }

    throw new InvalidOperationException($"Expected {nameof(ArgumentOutOfRangeException)} but no exception was thrown.");
}

Our test must now satisfy an additional condition in that the exception message, ex.Message, must contain the string, "must be greater than or equal to zero." Once again I've used Assert.Contains, but any assertion appropriate for the situation in other frameworks will work just as well. After we do the Assert.Contains, we need to return from the method, otherwise the flow of execution will reach the bottom and throw the InvalidOperationException.

Wrapping Up

And there you have it! In this article we've gone over how to unit test our code that will throw exceptions in a deterministic way. We can either use xUnit's Assert.Throws<T>, which makes life while testing for exceptions pretty easy, or we could do the old fashioned test agnostic way of using try/catch blocks. While xUnit does give us some nice syntactic sugar for testing exceptions, we can make the try/catch approach work equally well.

I've posted the code and testing project on my GitHub if you'd like to check it out.

I hope you find this article useful, and as always, happy coding!

Read Next

Unit Test Your C# Code Easily with xUnit and TDD image

January 12, 2020 by Chad

Unit Test Your C# Code Easily with xUnit and TDD

Unit testing your C# code has truly never been easier. Today I will introduce how to get up and running and unit testing your C# code in only a matter of seconds with one of the latest testing technologies, xUnit, using a Test Driven Development (TDD) approach.

Read Article
Multi-Tenanted Entity Framework Core Migration Deployment image

April 11, 2021 by Chad

Multi-Tenanted Entity Framework Core Migration Deployment

There's many ways to deploy pending Entity Framework Core (EF Core) migrations, especially for multi-tenanted scenarios. In this post, I'll demonstrate a strategy to efficiently apply pending EF Core 6 migrations using a .NET 6 console app.

Read Article
Multi-Tenanted Entity Framework 6 Migration Deployment image

April 10, 2021 by Chad

Multi-Tenanted Entity Framework 6 Migration Deployment

There's many ways to deploy pending Entity Framework 6 (EF6) migrations, especially for multi-tenanted production scenarios. In this post, I'll demonstrate a strategy to efficiently apply pending migrations using a .NET 6 console app.

Read Article