XUnit and Exceptions With async Task

517-24.jpg

 

When a business object requires catching exceptions generated by wrong property values, XUnit tests aren't as easy to write.

Recently, I wrote XUnit tests for a business object that requires catching exceptions generated by wrong property values in synchronous and asynchronous calls. This post includes several examples. The full code is accessible on GitHub. Here, I will use the approach described in Richard Banks' post, Stop Using Assert.Throws in Your BDD Unit Tests.

Let's describe objects that will be used for demonstration. The Data сlass describes the simple object with one property that throws an exception on negative values:

public class Data
{
  private readonly object _lockObject = new object();

  private int _state;

  public int State
  {
    get { return _state; }
    set
    {
      lock (_lockObject)
      {
        if (value < 0)
          throw new ArgumentOutOfRangeException(
            nameof(State),
            "State should be positive");
        _state = value;
        // some inner changes
      }
    }
  }
}Code language: JavaScript (javascript)

Let's write a simple test that assigns positive values and doesn't throw an exception:

[Theory]
[InlineData(0)]
[InlineData(1)]
public void Data_ShouldAccept_NonNegativeValue(int state)
{
  Data data = null;
  var exception = Record.Exception(() =>
  {
    data = new Data();
    data.State = state;
  });
  data.Should().NotBeNull();
  exception.Should().BeNull();
}Code language: PHP (php)

All tests are executed successfully and the exception is not thrown!

Web Development Services and Solutions Require the Most Reliable Partners Explore how Svitla Systems can guide your web development journey with expertise and innovative solutions. Contact Us

Now, let's consider the test that assigns negative state and throws an exception:

[Theory]
[InlineData(-1)]
public void Data_ShouldThrow_ExceptionOnNegativeValueAndReturnNullObject(int state)
{
  Data data = null;
  Action task = () =>
    {
      data = new Data
      {
        State = state
      };
    };
  var exception = Record.Exception(task);
  data.Should().BeNull();
  exception.Should().NotBeNull();
  exception.Message.Should().Be(ExceptionMessage);
}Code language: PHP (php)

As the Data class is designed to be thread-safe, we need tests that accesses  Data.State asynchronously.

Note that the used method Record.ExceptionAsync returns a value of type Task and marked as can be null. That is why the returned result is checked against a null value. Then, we check for the inner exception:

[Fact]
public void Data_ShouldNotThrow_ExceptionOnNonNegativeValueInAsync()
{
  var data = new Data();
  var task = Task.Run(() =>
        {
          for (var pos = 5; pos >= 0; pos--)
          {
            data.State = pos;
          }
        });
  var taskException = Record.ExceptionAsync(async () => await task);
  data.Should().NotBeNull();
  data.State.Should().Be(0);
  taskException.Should().NotBeNull();
  taskException.Result.Should().BeNull();
}Code language: JavaScript (javascript)

Further, the next test correctly catches the generated exception:

 
[Fact]
public void Data_ShouldThrow_ExceptionOnNegativeValueInAsync()
{
  var data = new Data();
  var task = Task.Run(() =>
        {
          for (var pos = 1; pos >= -2; pos--)
          {
            data.State = pos;
          }
        });
  var exception = Record.ExceptionAsync(async () => await task);
  data.Should().NotBeNull();
  data.State.Should().Be(0);
  exception.Should().NotBeNull();
  exception.Result.Should().NotBeNull();
  exception.Result.Message.Should().Be(ExceptionMessage);
}
Code language: PHP (php)

The similar test could be written with two asynchronous tasks:

[Fact]
public void Data_ShouldThrow_ExceptionOnNegativeStateInTwoAsyncTasks()
{
  var data = new Data();
  var tasks = new Task[]
    {
      Task.Run(() =>
        {
          for (var pos = 0; pos < 10; pos++)
          {
            data.State += 1;
          }
        }), 
      Task.Run(() =>
        {
          for (var pos = 0; pos < 20; pos++)
          {
            data.State -= 1;
          }
        }), 
    };
  var exception = Record.ExceptionAsync(async () =>
        await Task.WhenAll(tasks));
  data.Should().NotBeNull();
  exception.Should().NotBeNull();
  exception.Result.Should().NotBeNull();
  exception.Result.Message.Should().Be(ExceptionMessage);
}Code language: PHP (php)

That's it! You've now created XUnit tests for a business object that requires catching exceptions generated by wrong property values in synchronous and asynchronous calls.

FAQ

How to throw exception in xUnit test in C#?

To throw (and assert) exceptions in an xUnit C# test, wrap the code under test in a delegate and use Record.Exception/Record.ExceptionAsync instead of Assert.Throws. For synchronous code, you do something like var ex = Record.Exception(() => obj.State = -1); and then assert on ex (e.g., ex.Should().NotBeNull(); ex.Message.Should().Be("...");). For asynchronous code, run the task and call var exTask = Record.ExceptionAsync(async () => await task);, then check exTask.Result for null or specific exception details. This pattern works well for both simple property assignments and more complex, multi-threaded scenarios, while keeping tests readable and BDD-friendly.

How to pass parameters to xUnit test in C#?

To pass parameters to an xUnit test C#, use the [Theory] attribute, not the normal [Fact] attribute. You give it specific test cases using [InlineData]. Here, each instance denotes a set of values that will be passed into the parameters of the test method. This way, you may run the same logic with different inputs inside one and the same method.

How to throw an exception in a test class?

To raise an exception inside a test class, define a property or method. Mostly, validation logic is placed in the form of if statements, checking whether values are valid or not. For instance, one can use a setter to throw an ArgumentOutOfRangeException if the state property is set to any value less than zero. This ensures that the business object enforces its rules, which can later be verified using the recording methods provided by xUnit. Organize your class this way so that exceptions become predictable responses to invalid data, both synchronously and asynchronously.

Why throw exceptions in C#?

Exceptions help enforce business rules and ensure the object’s state validity by not allowing processing of invalid data. For example, an ArgumentOutOfRangeException exception can be thrown when a property is being set with an unsupported value, thereby ensuring that the application does not get into a broken state. This mechanism provides a highly structured way to signal errors to the calling code, enabling specific problems to be caught and handled during both synchronous and asynchronous operations. In the end, it makes the code more solid and easier to debug by explicitly indicating where and why it failed.

How do you handle exceptions in asynchronous C# code, especially when multiple tasks are involved?

As with typical asynchronous C# code, you handle exceptions by awaiting the Task (or Task.WhenAll in the case of multiple tasks) and checking the returned exception, most often an AggregateException. Similarly, within a test-wrap, the awaited call is inside a delegate and uses something like Record.ExceptionAsync(async () => await taskOrWhenAll) to capture any thrown exception, then assert that it is null or has the expected error. When there are multiple tasks involved, exceptions from any of those tasks will be surfaced with the Task.WhenAll tasks are complete, a test looking at that captured exception (and its InnerException/InnerExceptions) validates specifically which one failed. This pattern ensures validation reliably, both that exceptions do occur when they should, and that they contain correct details, even in concurrency scenarios.