Testing Exceptions with xUnit and Actions
Date Published: 14 April 2021
When you're writing unit tests for a method, it's a good idea to test various failure conditions ("sad paths") in addition to testing the expected, everything is working condition ("happy path"). In particular, if you have a method that may throw exceptions, especially if they're custom domain exceptions, you should be sure you're testing this behavior. Another common source of exceptions is guard clauses, which are used to keep your method clean while ensuring its inputs adhere to the method's expectations.
Unlike NUnit, which mainly uses attributes for expected exceptions, and MSTest, which has little built-in support at all, xUnit provides an Assert.Throws<T>
method that is used to test for expected exceptions (NUnit 3 also has a similar method). A simple example looks like this:
Customer customer = null;
Assert.Throws<NullReferenceException>(() => customer.LastName);
In addition to this simple usage, the method also returns the captured exception, so you can make additional assertions about the message or other properties of the exception:
var customer = new Customer();
var caughtException = Assert.Throws<NameRequiredException>(() => customer.UpdateName("", ""));
Assert.Equal("A valid name must be supplied.", caughtException.Message);
Arrange, Act, Assert and Exceptions
Many tests use the Arrange, Act, Assert, or AAA testing pattern. Using this approach, tests generally have this form:
public class Customer_UpdateName
{
[Fact]
public void ThrowsExceptionGivenInvalidName
{
// Arrange
// Act
// Assert
}
}
Note: Tests should be named to that when they fail, the combination of their class name and method name clearly indicates the expectation that was not met. Read more about unit test naming conventions.
Let's fill in the details from the Customer.UpdateName()
test example, using the AAA template:
public class Customer_UpdateName
{
[Fact]
public void ThrowsExceptionGivenInvalidName
{
// Arrange
var customer = new Customer();
// Act
var caughtException = Assert.Throws<NameRequiredException>(() => customer.UpdateName("", ""));
// Assert
Assert.Equal("A valid name must be supplied.", caughtException.Message);
}
}
Do you see the problem? The Assert.Throws
line is both the Act step as well as one of the assertions. This makes the line both overly long and makes the test harder to read. We can fix this by using an Action
to represent the operation that is expected to throw an exception.
Using Actions with Assert.Throws
To help follow the AAA test pattern, represent the operation that is expected to throw an exception as a variable by using an Action
:
public class Customer_UpdateName
{
[Fact]
public void ThrowsExceptionGivenInvalidName
{
// Arrange
var customer = new Customer();
// Act
Action action = () => customer.UpdateName("", "");
// Assert
var caughtException = Assert.Throws<NameRequiredException>(action);
Assert.Equal("A valid name must be supplied.", caughtException.Message);
}
}
Now in this example, the action that is the heart of the test stands alone within the Act section of the test, and the Assert.Throws
line is much shorter and easier to follow.
FluentAssertions
If you're using FluentAssertions instead of the built-in assertions of your testing library, you would do this:
[Fact]
public void ThrowsExceptionGivenInvalidName
{
// Arrange
var customer = new Customer();
// Act
Action action = () => customer.UpdateName("", "");
// Assert
action.Should()
.Throw<NameRequiredException>()
.WithMessage("A valid name must be supplied.");
}
}
Thanks to @volkmarrigo for this suggestion!
Local Functions
Another options I learned recently from @snowfrogdev is to use a local function instead of just an Action
:
public class Customer_UpdateName
{
[Fact]
public void ThrowsExceptionGivenInvalidName
{
// Arrange
var customer = new Customer();
// Act - local function
void act() => customer.UpdateName("", "");
// Assert
var caughtException = Assert.Throws<NameRequiredException>(act);
Assert.Equal("A valid name must be supplied.", caughtException.Message);
}
}
I've been using the Action
approach recently, since my friend Shady Nagy (you should follow him on Twitter) pointed it out to me on a project we were working on together. This pattern is really simple to implement but I've found it makes tests for exceptions much cleaner.
Category - Browse all categories
About Ardalis
Software Architect
Steve is an experienced software architect and trainer, focusing on code quality and Domain-Driven Design with .NET.