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.
- Unit Testing Your C# Code with xUnit
- 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.
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.
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.
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.");
}
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.
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!
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!