diff --git a/TUnit.Assertions/Conditions/ExceptionPropertyAssertions.cs b/TUnit.Assertions/Conditions/ExceptionPropertyAssertions.cs
index 457859cb14..df9366496e 100644
--- a/TUnit.Assertions/Conditions/ExceptionPropertyAssertions.cs
+++ b/TUnit.Assertions/Conditions/ExceptionPropertyAssertions.cs
@@ -260,6 +260,107 @@ protected override string GetExpectation() =>
$"exception message to match {_matcher}";
}
+///
+/// Asserts that an exception's StackTrace property contains a specific substring.
+/// Chains after a Throws assertion.
+/// Example: await Assert.That(() => ThrowingMethod()).Throws<Exception>().WithStackTraceContaining("MyClass.MyMethod");
+///
+public class ExceptionStackTraceContainsAssertion : Assertion
+ where TException : Exception
+{
+ private readonly string _expectedSubstring;
+ private readonly StringComparison _comparison;
+
+ public ExceptionStackTraceContainsAssertion(
+ AssertionContext context,
+ string expectedSubstring,
+ StringComparison comparison = StringComparison.Ordinal)
+ : base(context)
+ {
+ _expectedSubstring = expectedSubstring;
+ _comparison = comparison;
+ }
+
+ protected override Task CheckAsync(EvaluationMetadata metadata)
+ {
+ var exception = metadata.Value;
+ var evaluationException = metadata.Exception;
+
+ if (evaluationException != null)
+ {
+ return Task.FromResult(AssertionResult.Failed($"threw {evaluationException.GetType().FullName}"));
+ }
+
+ if (exception == null)
+ {
+ return Task.FromResult(AssertionResult.Failed("no exception was thrown"));
+ }
+
+ if (exception.StackTrace == null)
+ {
+ return Task.FromResult(AssertionResult.Failed("exception stack trace was null"));
+ }
+
+ if (exception.StackTrace.Contains(_expectedSubstring, _comparison))
+ {
+ return AssertionResult._passedTask;
+ }
+
+ return Task.FromResult(AssertionResult.Failed($"exception stack trace was \"{exception.StackTrace}\""));
+ }
+
+ protected override string GetExpectation() =>
+ $"exception stack trace to contain \"{_expectedSubstring}\"";
+}
+
+///
+/// Asserts that an exception has an inner exception of the specified type.
+/// Chains after a Throws assertion.
+/// Example: await Assert.That(() => ThrowingMethod()).Throws<Exception>().WithInnerException<InvalidOperationException>();
+///
+public class ExceptionInnerExceptionOfTypeAssertion : Assertion
+ where TException : Exception
+ where TInnerException : Exception
+{
+ public ExceptionInnerExceptionOfTypeAssertion(
+ AssertionContext context)
+ : base(context)
+ {
+ }
+
+ protected override Task CheckAsync(EvaluationMetadata metadata)
+ {
+ var exception = metadata.Value;
+ var evaluationException = metadata.Exception;
+
+ if (evaluationException != null)
+ {
+ return Task.FromResult(AssertionResult.Failed($"threw {evaluationException.GetType().FullName}"));
+ }
+
+ if (exception == null)
+ {
+ return Task.FromResult(AssertionResult.Failed("no exception was thrown"));
+ }
+
+ if (exception.InnerException == null)
+ {
+ return Task.FromResult(AssertionResult.Failed("exception has no inner exception"));
+ }
+
+ if (exception.InnerException is not TInnerException)
+ {
+ return Task.FromResult(AssertionResult.Failed(
+ $"inner exception was {exception.InnerException.GetType().Name} instead of {typeof(TInnerException).Name}"));
+ }
+
+ return AssertionResult._passedTask;
+ }
+
+ protected override string GetExpectation() =>
+ $"exception to have inner exception of type {typeof(TInnerException).Name}";
+}
+
///
/// Asserts that an ArgumentException has a specific parameter name.
///
diff --git a/TUnit.Assertions/Conditions/ThrowsAssertion.cs b/TUnit.Assertions/Conditions/ThrowsAssertion.cs
index c43f4b85f6..3b4de430b7 100644
--- a/TUnit.Assertions/Conditions/ThrowsAssertion.cs
+++ b/TUnit.Assertions/Conditions/ThrowsAssertion.cs
@@ -200,6 +200,37 @@ public ExceptionParameterNameAssertion WithParameterName(string expe
return new ExceptionParameterNameAssertion(Context, expectedParameterName);
}
+ ///
+ /// Asserts that the exception has an inner exception of the specified type.
+ /// Example: await Assert.That(() => ThrowingMethod()).Throws<Exception>().WithInnerException<InvalidOperationException>();
+ ///
+ public ExceptionInnerExceptionOfTypeAssertion WithInnerException()
+ where TInnerException : Exception
+ {
+ Context.ExpressionBuilder.Append($".WithInnerException<{typeof(TInnerException).Name}>()");
+ return new ExceptionInnerExceptionOfTypeAssertion(Context);
+ }
+
+ ///
+ /// Asserts that the exception's stack trace contains the specified substring.
+ /// Example: await Assert.That(() => ThrowingMethod()).Throws<Exception>().WithStackTraceContaining("MyClass.MyMethod");
+ ///
+ public ExceptionStackTraceContainsAssertion WithStackTraceContaining(string expectedSubstring)
+ {
+ Context.ExpressionBuilder.Append($".WithStackTraceContaining(\"{expectedSubstring}\")");
+ return new ExceptionStackTraceContainsAssertion(Context, expectedSubstring);
+ }
+
+ ///
+ /// Asserts that the exception's stack trace contains the specified substring using the specified comparison.
+ /// Example: await Assert.That(() => ThrowingMethod()).Throws<Exception>().WithStackTraceContaining("MyClass", StringComparison.OrdinalIgnoreCase);
+ ///
+ public ExceptionStackTraceContainsAssertion WithStackTraceContaining(string expectedSubstring, StringComparison comparison)
+ {
+ Context.ExpressionBuilder.Append($".WithStackTraceContaining(\"{expectedSubstring}\", StringComparison.{comparison})");
+ return new ExceptionStackTraceContainsAssertion(Context, expectedSubstring, comparison);
+ }
+
///
/// Adds runtime Type-based exception checking for non-generic Throws scenarios.
/// Returns a specialized assertion that validates against the provided Type.
@@ -341,6 +372,37 @@ public ExceptionParameterNameAssertion WithParameterName(string expe
Context.ExpressionBuilder.Append($".WithParameterName(\"{expectedParameterName}\")");
return new ExceptionParameterNameAssertion(Context, expectedParameterName, requireExactType: true);
}
+
+ ///
+ /// Asserts that the exception has an inner exception of the specified type.
+ /// Example: await Assert.That(() => ThrowingMethod()).ThrowsExactly<Exception>().WithInnerException<InvalidOperationException>();
+ ///
+ public ExceptionInnerExceptionOfTypeAssertion WithInnerException()
+ where TInnerException : Exception
+ {
+ Context.ExpressionBuilder.Append($".WithInnerException<{typeof(TInnerException).Name}>()");
+ return new ExceptionInnerExceptionOfTypeAssertion(Context);
+ }
+
+ ///
+ /// Asserts that the exception's stack trace contains the specified substring.
+ /// Example: await Assert.That(() => ThrowingMethod()).ThrowsExactly<Exception>().WithStackTraceContaining("MyClass.MyMethod");
+ ///
+ public ExceptionStackTraceContainsAssertion WithStackTraceContaining(string expectedSubstring)
+ {
+ Context.ExpressionBuilder.Append($".WithStackTraceContaining(\"{expectedSubstring}\")");
+ return new ExceptionStackTraceContainsAssertion(Context, expectedSubstring);
+ }
+
+ ///
+ /// Asserts that the exception's stack trace contains the specified substring using the specified comparison.
+ /// Example: await Assert.That(() => ThrowingMethod()).ThrowsExactly<Exception>().WithStackTraceContaining("MyClass", StringComparison.OrdinalIgnoreCase);
+ ///
+ public ExceptionStackTraceContainsAssertion WithStackTraceContaining(string expectedSubstring, StringComparison comparison)
+ {
+ Context.ExpressionBuilder.Append($".WithStackTraceContaining(\"{expectedSubstring}\", StringComparison.{comparison})");
+ return new ExceptionStackTraceContainsAssertion(Context, expectedSubstring, comparison);
+ }
}
///
diff --git a/TUnit.Assertions/Extensions/AssertionExtensions.cs b/TUnit.Assertions/Extensions/AssertionExtensions.cs
index d3bada1e50..c2a380f459 100644
--- a/TUnit.Assertions/Extensions/AssertionExtensions.cs
+++ b/TUnit.Assertions/Extensions/AssertionExtensions.cs
@@ -1057,6 +1057,80 @@ public static ExceptionParameterNameAssertion WithParameterName(source.Context, expectedParameterName);
}
+ ///
+ /// Asserts that the exception's stack trace contains the specified substring.
+ /// Works after Throws assertions.
+ /// Example: await Assert.That(() => ThrowingMethod()).Throws<Exception>().And.WithStackTraceContaining("MyClass.MyMethod");
+ ///
+ public static ExceptionStackTraceContainsAssertion WithStackTraceContaining(
+ this IAssertionSource source,
+ string expectedSubstring,
+ [CallerArgumentExpression(nameof(expectedSubstring))] string? expression = null)
+ where TException : Exception
+ {
+ source.Context.ExpressionBuilder.Append($".WithStackTraceContaining({expression})");
+ return new ExceptionStackTraceContainsAssertion(source.Context, expectedSubstring);
+ }
+
+ ///
+ /// Asserts that the exception's stack trace contains the specified substring using the specified comparison.
+ /// Works after Throws assertions.
+ /// Example: await Assert.That(() => ThrowingMethod()).Throws<Exception>().And.WithStackTraceContaining("MyClass", StringComparison.OrdinalIgnoreCase);
+ ///
+ public static ExceptionStackTraceContainsAssertion WithStackTraceContaining(
+ this IAssertionSource source,
+ string expectedSubstring,
+ StringComparison comparison,
+ [CallerArgumentExpression(nameof(expectedSubstring))] string? expression = null)
+ where TException : Exception
+ {
+ source.Context.ExpressionBuilder.Append($".WithStackTraceContaining({expression}, StringComparison.{comparison})");
+ return new ExceptionStackTraceContainsAssertion(source.Context, expectedSubstring, comparison);
+ }
+
+ ///
+ /// Asserts that the exception has an inner exception of the specified type.
+ /// Works after Throws assertions.
+ /// Example: await Assert.That(() => ThrowingMethod()).Throws<Exception>().And.WithInnerException<InvalidOperationException>();
+ ///
+ public static ExceptionInnerExceptionOfTypeAssertion WithInnerException(
+ this IAssertionSource source)
+ where TException : Exception
+ where TInnerException : Exception
+ {
+ source.Context.ExpressionBuilder.Append($".WithInnerException<{typeof(TInnerException).Name}>()");
+ return new ExceptionInnerExceptionOfTypeAssertion(source.Context);
+ }
+
+ ///
+ /// Alias for WithMessage - asserts that the exception message exactly equals the specified string.
+ /// Works after Throws assertions.
+ /// Example: await Assert.That(() => ThrowingMethod()).Throws<Exception>().And.HasMessage("exact message");
+ ///
+ public static ExceptionMessageEqualsAssertion HasMessage(
+ this IAssertionSource source,
+ string expectedMessage,
+ [CallerArgumentExpression(nameof(expectedMessage))] string? expression = null)
+ where TException : Exception
+ {
+ return source.WithMessage(expectedMessage, expression);
+ }
+
+ ///
+ /// Alias for WithMessage - asserts that the exception message exactly equals the specified string using the specified comparison.
+ /// Works after Throws assertions.
+ /// Example: await Assert.That(() => ThrowingMethod()).Throws<Exception>().And.HasMessage("exact message", StringComparison.OrdinalIgnoreCase);
+ ///
+ public static ExceptionMessageEqualsAssertion HasMessage(
+ this IAssertionSource source,
+ string expectedMessage,
+ StringComparison comparison,
+ [CallerArgumentExpression(nameof(expectedMessage))] string? expression = null)
+ where TException : Exception
+ {
+ return source.WithMessage(expectedMessage, comparison, expression);
+ }
+
public static ThrowsAssertion Throws(this DelegateAssertion source) where TException : Exception
{
var iface = (IAssertionSource