Exception Assertions
TUnit provides comprehensive assertions for testing that code throws (or doesn't throw) exceptions, with rich support for validating exception types, messages, and properties.
Basic Exception Assertionsâ
Throws<TException>â
Tests that a delegate throws a specific exception type (or a subclass):
[Test]
public async Task Code_Throws_Exception()
{
await Assert.That(() => int.Parse("not a number"))
.Throws<FormatException>();
}
Works with any exception type:
[Test]
public async Task Various_Exception_Types()
{
await Assert.That(() => throw new InvalidOperationException())
.Throws<InvalidOperationException>();
await Assert.That(() => throw new ArgumentNullException())
.Throws<ArgumentNullException>();
await Assert.That(() => File.ReadAllText("nonexistent.txt"))
.Throws<FileNotFoundException>();
}
ThrowsExactly<TException>â
Tests that a delegate throws the exact exception type (not a subclass):
[Test]
public async Task Throws_Exact_Type()
{
await Assert.That(() => throw new ArgumentNullException())
.ThrowsExactly<ArgumentNullException>();
// This would fail - ArgumentNullException is a subclass of ArgumentException
// await Assert.That(() => throw new ArgumentNullException())
// .ThrowsExactly<ArgumentException>();
}
Throws (Runtime Type)â
Use when the exception type is only known at runtime:
[Test]
public async Task Throws_Runtime_Type()
{
Type exceptionType = typeof(InvalidOperationException);
await Assert.That(() => throw new InvalidOperationException())
.Throws(exceptionType);
}
ThrowsNothingâ
Tests that code does not throw any exception:
[Test]
public async Task Code_Does_Not_Throw()
{
await Assert.That(() => int.Parse("42"))
.ThrowsNothing();
await Assert.That(() => ValidateInput("valid"))
.ThrowsNothing();
}
Async Exception Assertionsâ
For async operations, use async delegates:
[Test]
public async Task Async_Throws_Exception()
{
await Assert.That(async () => await FailingOperationAsync())
.Throws<HttpRequestException>();
}
[Test]
public async Task Async_Does_Not_Throw()
{
await Assert.That(async () => await SuccessfulOperationAsync())
.ThrowsNothing();
}
Exception Message Assertionsâ
WithMessageâ
Tests that the exception has an exact message:
[Test]
public async Task Exception_With_Exact_Message()
{
await Assert.That(() => throw new InvalidOperationException("Operation failed"))
.Throws<InvalidOperationException>()
.WithMessage("Operation failed");
}
WithMessageContainingâ
Tests that the exception message contains a substring:
[Test]
public async Task Exception_Message_Contains()
{
await Assert.That(() => throw new ArgumentException("The parameter 'userId' is invalid"))
.Throws<ArgumentException>()
.WithMessageContaining("userId");
}
Case-Insensitiveâ
[Test]
public async Task Message_Contains_Ignoring_Case()
{
await Assert.That(() => throw new Exception("ERROR: Failed"))
.Throws<Exception>()
.WithMessageContaining("error")
.IgnoringCase();
}
WithMessageNotContainingâ
Tests that the exception message does not contain a substring:
[Test]
public async Task Message_Does_Not_Contain()
{
await Assert.That(() => throw new Exception("User error"))
.Throws<Exception>()
.WithMessageNotContaining("system");
}
WithMessageMatchingâ
Tests that the exception message matches a pattern:
[Test]
public async Task Message_Matches_Pattern()
{
await Assert.That(() => throw new Exception("Error code: 12345"))
.Throws<Exception>()
.WithMessageMatching("Error code: *");
}
Or with a StringMatcher:
[Test]
public async Task Message_Matches_With_Matcher()
{
var matcher = new StringMatcher("Error * occurred", caseSensitive: false);
await Assert.That(() => throw new Exception("Error 500 occurred"))
.Throws<Exception>()
.WithMessageMatching(matcher);
}
ArgumentException Specificâ
WithParameterNameâ
For ArgumentException and its subclasses, you can assert on the parameter name:
[Test]
public async Task ArgumentException_With_Parameter_Name()
{
await Assert.That(() => ValidateUser(null!))
.Throws<ArgumentNullException>()
.WithParameterName("user");
}
void ValidateUser(User user)
{
if (user == null)
throw new ArgumentNullException(nameof(user));
}
Combine with message assertions:
[Test]
public async Task ArgumentException_Parameter_And_Message()
{
await Assert.That(() => SetAge(-1))
.Throws<ArgumentOutOfRangeException>()
.WithParameterName("age")
.WithMessageContaining("must be positive");
}
void SetAge(int age)
{
if (age < 0)
throw new ArgumentOutOfRangeException(nameof(age), "Age must be positive");
}
Inner Exception Assertionsâ
WithInnerExceptionâ
Assert on the inner exception:
[Test]
public async Task Exception_With_Inner_Exception()
{
await Assert.That(() => {
try
{
int.Parse("not a number");
}
catch (Exception ex)
{
throw new InvalidOperationException("Processing failed", ex);
}
})
.Throws<InvalidOperationException>()
.WithInnerException();
}
Chain to assert on the inner exception type:
[Test]
public async Task Inner_Exception_Type()
{
await Assert.That(() => ThrowWithInner())
.Throws<InvalidOperationException>()
.WithInnerException()
.Throws<FormatException>();
}
void ThrowWithInner()
{
try
{
int.Parse("abc");
}
catch (Exception ex)
{
throw new InvalidOperationException("Outer", ex);
}
}
Practical Examplesâ
Validation Exceptionsâ
[Test]
public async Task Validate_Email_Throws()
{
await Assert.That(() => ValidateEmail("invalid-email"))
.Throws<ArgumentException>()
.WithParameterName("email")
.WithMessageContaining("valid email");
}
Null Argument Checksâ
[Test]
public async Task Null_Argument_Throws()
{
await Assert.That(() => ProcessData(null!))
.Throws<ArgumentNullException>()
.WithParameterName("data");
}
File Operationsâ
[Test]
public async Task File_Not_Found()
{
await Assert.That(() => File.ReadAllText("nonexistent.txt"))
.Throws<FileNotFoundException>()
.WithMessageContaining("nonexistent.txt");
}
Network Operationsâ
[Test]
public async Task HTTP_Request_Fails()
{
await Assert.That(async () => await _client.GetAsync("http://invalid-url"))
.Throws<HttpRequestException>();
}
Database Operationsâ
[Test]
public async Task Duplicate_Key_Violation()
{
await Assert.That(async () => await InsertDuplicateAsync())
.Throws<DbUpdateException>()
.WithMessageContaining("duplicate key");
}
Division by Zeroâ
[Test]
public async Task Division_By_Zero()
{
await Assert.That(() => {
int a = 10;
int b = 0;
return a / b;
})
.Throws<DivideByZeroException>();
}
Index Out of Rangeâ
[Test]
public async Task Array_Index_Out_Of_Range()
{
var array = new[] { 1, 2, 3 };
await Assert.That(() => array[10])
.Throws<IndexOutOfRangeException>();
}
Invalid Castâ
[Test]
public async Task Invalid_Cast()
{
object obj = "string";
await Assert.That(() => (int)obj)
.Throws<InvalidCastException>();
}
Custom Exceptionsâ
public class BusinessRuleException : Exception
{
public string RuleCode { get; }
public BusinessRuleException(string ruleCode, string message)
: base(message)
{
RuleCode = ruleCode;
}
}
[Test]
public async Task Custom_Exception_With_Properties()
{
var exception = await Assert.That(() =>
throw new BusinessRuleException("BR001", "Business rule violated"))
.Throws<BusinessRuleException>();
// Can't directly assert on exception properties yet, but you can access them
await Assert.That(exception.RuleCode).IsEqualTo("BR001");
await Assert.That(exception.Message).Contains("Business rule");
}
Testing Multiple Operationsâ
Using Assert.Multipleâ
[Test]
public async Task Multiple_Exception_Scenarios()
{
await using (Assert.Multiple())
{
await Assert.That(() => int.Parse("abc"))
.Throws<FormatException>();
await Assert.That(() => int.Parse("999999999999999999999"))
.Throws<OverflowException>();
await Assert.That(() => int.Parse("42"))
.ThrowsNothing();
}
}
Exception Inheritanceâ
When using Throws<T>(), subclasses are accepted:
[Test]
public async Task Exception_Inheritance()
{
// ArgumentNullException inherits from ArgumentException
await Assert.That(() => throw new ArgumentNullException())
.Throws<ArgumentException>(); // â
Passes
await Assert.That(() => throw new ArgumentNullException())
.Throws<ArgumentNullException>(); // â
Also passes
}
Use ThrowsExactly<T>() if you need the exact type:
[Test]
public async Task Exact_Exception_Type()
{
// This fails - ArgumentNullException is not exactly ArgumentException
// await Assert.That(() => throw new ArgumentNullException())
// .ThrowsExactly<ArgumentException>();
await Assert.That(() => throw new ArgumentException())
.ThrowsExactly<ArgumentException>(); // â
Passes
}
Aggregate Exceptionsâ
[Test]
public async Task Aggregate_Exception()
{
await Assert.That(() => {
var task1 = Task.Run(() => throw new InvalidOperationException());
var task2 = Task.Run(() => throw new ArgumentException());
Task.WaitAll(task1, task2);
})
.Throws<AggregateException>();
}
Chaining Exception Assertionsâ
[Test]
public async Task Chained_Exception_Assertions()
{
await Assert.That(() => ValidateInput(""))
.Throws<ArgumentException>()
.WithParameterName("input")
.WithMessageContaining("cannot be empty")
.WithMessageNotContaining("null");
}
Testing that No Exception is Thrownâ
ThrowsNothing vs Try-Catchâ
[Test]
public async Task Explicit_No_Exception()
{
// Using ThrowsNothing
await Assert.That(() => SafeOperation())
.ThrowsNothing();
// Alternative: just call it
SafeOperation(); // If it throws, the test fails
}
Common Patternsâ
Expected Failuresâ
[Test]
public async Task Expected_Validation_Failure()
{
var invalidUser = new User { Age = -1 };
await Assert.That(() => ValidateUser(invalidUser))
.Throws<ValidationException>()
.WithMessageContaining("Age");
}
Defensive Programmingâ
[Test]
public async Task Guard_Clause_Validation()
{
await Assert.That(() => new Service(null!))
.Throws<ArgumentNullException>()
.WithParameterName("dependency");
}
State Validationâ
[Test]
public async Task Invalid_State_Operation()
{
var connection = new Connection();
// Don't connect
await Assert.That(() => connection.SendData("test"))
.Throws<InvalidOperationException>()
.WithMessageContaining("not connected");
}
Configuration Errorsâ
[Test]
public async Task Missing_Configuration()
{
await Assert.That(() => LoadConfiguration("invalid.json"))
.Throws<ConfigurationException>()
.WithMessageContaining("invalid.json");
}
Timeout Exceptionsâ
[Test]
public async Task Operation_Timeout()
{
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(100));
await Assert.That(async () => await LongRunningOperationAsync(cts.Token))
.Throws<TaskCanceledException>();
}
Re-throwing Exceptionsâ
[Test]
public async Task Wrapper_Exception()
{
await Assert.That(() => {
try
{
RiskyOperation();
}
catch (Exception ex)
{
throw new ApplicationException("Operation failed", ex);
}
})
.Throws<ApplicationException>()
.WithInnerException();
}
Exception Assertions with Async/Awaitâ
[Test]
public async Task Async_Exception_Handling()
{
await Assert.That(async () => {
await Task.Delay(10);
throw new InvalidOperationException("Async failure");
})
.Throws<InvalidOperationException>()
.WithMessageContaining("Async failure");
}
See Alsoâ
- Tasks & Async - Testing async operations and task state
- Types - Type checking for exception types
- Strings - String assertions for exception messages