Testing Exceptions with xUnit and Actions

Date Published: 14 April 2021

Testing Exceptions with xUnit and Actions

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.

Steve Smith

About Ardalis

Software Architect

Steve is an experienced software architect and trainer, focusing on code quality and Domain-Driven Design with .NET.


Ardalis

Copyright © 2021