Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions TUnit.Assertions/Conditions/ExceptionPropertyAssertions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,107 @@ protected override string GetExpectation() =>
$"exception message to match {_matcher}";
}

/// <summary>
/// Asserts that an exception's StackTrace property contains a specific substring.
/// Chains after a Throws assertion.
/// Example: await Assert.That(() => ThrowingMethod()).Throws&lt;Exception&gt;().WithStackTraceContaining("MyClass.MyMethod");
/// </summary>
public class ExceptionStackTraceContainsAssertion<TException> : Assertion<TException>
where TException : Exception
{
private readonly string _expectedSubstring;
private readonly StringComparison _comparison;

public ExceptionStackTraceContainsAssertion(
AssertionContext<TException> context,
string expectedSubstring,
StringComparison comparison = StringComparison.Ordinal)
: base(context)
{
_expectedSubstring = expectedSubstring;
_comparison = comparison;
}

protected override Task<AssertionResult> CheckAsync(EvaluationMetadata<TException> 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}\"";
}

/// <summary>
/// Asserts that an exception has an inner exception of the specified type.
/// Chains after a Throws assertion.
/// Example: await Assert.That(() => ThrowingMethod()).Throws&lt;Exception&gt;().WithInnerException&lt;InvalidOperationException&gt;();
/// </summary>
public class ExceptionInnerExceptionOfTypeAssertion<TException, TInnerException> : Assertion<TException>
where TException : Exception
where TInnerException : Exception
{
public ExceptionInnerExceptionOfTypeAssertion(
AssertionContext<TException> context)
: base(context)
{
}

protected override Task<AssertionResult> CheckAsync(EvaluationMetadata<TException> 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}";
}

/// <summary>
/// Asserts that an ArgumentException has a specific parameter name.
/// </summary>
Expand Down
62 changes: 62 additions & 0 deletions TUnit.Assertions/Conditions/ThrowsAssertion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,37 @@ public ExceptionParameterNameAssertion<TException> WithParameterName(string expe
return new ExceptionParameterNameAssertion<TException>(Context, expectedParameterName);
}

/// <summary>
/// Asserts that the exception has an inner exception of the specified type.
/// Example: await Assert.That(() => ThrowingMethod()).Throws&lt;Exception&gt;().WithInnerException&lt;InvalidOperationException&gt;();
/// </summary>
public ExceptionInnerExceptionOfTypeAssertion<TException, TInnerException> WithInnerException<TInnerException>()
where TInnerException : Exception
{
Context.ExpressionBuilder.Append($".WithInnerException<{typeof(TInnerException).Name}>()");
return new ExceptionInnerExceptionOfTypeAssertion<TException, TInnerException>(Context);
}

/// <summary>
/// Asserts that the exception's stack trace contains the specified substring.
/// Example: await Assert.That(() => ThrowingMethod()).Throws&lt;Exception&gt;().WithStackTraceContaining("MyClass.MyMethod");
/// </summary>
public ExceptionStackTraceContainsAssertion<TException> WithStackTraceContaining(string expectedSubstring)
{
Context.ExpressionBuilder.Append($".WithStackTraceContaining(\"{expectedSubstring}\")");
return new ExceptionStackTraceContainsAssertion<TException>(Context, expectedSubstring);
}

/// <summary>
/// Asserts that the exception's stack trace contains the specified substring using the specified comparison.
/// Example: await Assert.That(() => ThrowingMethod()).Throws&lt;Exception&gt;().WithStackTraceContaining("MyClass", StringComparison.OrdinalIgnoreCase);
/// </summary>
public ExceptionStackTraceContainsAssertion<TException> WithStackTraceContaining(string expectedSubstring, StringComparison comparison)
{
Context.ExpressionBuilder.Append($".WithStackTraceContaining(\"{expectedSubstring}\", StringComparison.{comparison})");
return new ExceptionStackTraceContainsAssertion<TException>(Context, expectedSubstring, comparison);
}

/// <summary>
/// Adds runtime Type-based exception checking for non-generic Throws scenarios.
/// Returns a specialized assertion that validates against the provided Type.
Expand Down Expand Up @@ -341,6 +372,37 @@ public ExceptionParameterNameAssertion<TException> WithParameterName(string expe
Context.ExpressionBuilder.Append($".WithParameterName(\"{expectedParameterName}\")");
return new ExceptionParameterNameAssertion<TException>(Context, expectedParameterName, requireExactType: true);
}

/// <summary>
/// Asserts that the exception has an inner exception of the specified type.
/// Example: await Assert.That(() => ThrowingMethod()).ThrowsExactly&lt;Exception&gt;().WithInnerException&lt;InvalidOperationException&gt;();
/// </summary>
public ExceptionInnerExceptionOfTypeAssertion<TException, TInnerException> WithInnerException<TInnerException>()
where TInnerException : Exception
{
Context.ExpressionBuilder.Append($".WithInnerException<{typeof(TInnerException).Name}>()");
return new ExceptionInnerExceptionOfTypeAssertion<TException, TInnerException>(Context);
}

/// <summary>
/// Asserts that the exception's stack trace contains the specified substring.
/// Example: await Assert.That(() => ThrowingMethod()).ThrowsExactly&lt;Exception&gt;().WithStackTraceContaining("MyClass.MyMethod");
/// </summary>
public ExceptionStackTraceContainsAssertion<TException> WithStackTraceContaining(string expectedSubstring)
{
Context.ExpressionBuilder.Append($".WithStackTraceContaining(\"{expectedSubstring}\")");
return new ExceptionStackTraceContainsAssertion<TException>(Context, expectedSubstring);
}

/// <summary>
/// Asserts that the exception's stack trace contains the specified substring using the specified comparison.
/// Example: await Assert.That(() => ThrowingMethod()).ThrowsExactly&lt;Exception&gt;().WithStackTraceContaining("MyClass", StringComparison.OrdinalIgnoreCase);
/// </summary>
public ExceptionStackTraceContainsAssertion<TException> WithStackTraceContaining(string expectedSubstring, StringComparison comparison)
{
Context.ExpressionBuilder.Append($".WithStackTraceContaining(\"{expectedSubstring}\", StringComparison.{comparison})");
return new ExceptionStackTraceContainsAssertion<TException>(Context, expectedSubstring, comparison);
}
}

/// <summary>
Expand Down
74 changes: 74 additions & 0 deletions TUnit.Assertions/Extensions/AssertionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1057,6 +1057,80 @@ public static ExceptionParameterNameAssertion<TException> WithParameterName<TExc
return new ExceptionParameterNameAssertion<TException>(source.Context, expectedParameterName);
}

/// <summary>
/// Asserts that the exception's stack trace contains the specified substring.
/// Works after Throws assertions.
/// Example: await Assert.That(() => ThrowingMethod()).Throws&lt;Exception&gt;().And.WithStackTraceContaining("MyClass.MyMethod");
/// </summary>
public static ExceptionStackTraceContainsAssertion<TException> WithStackTraceContaining<TException>(
this IAssertionSource<TException> source,
string expectedSubstring,
[CallerArgumentExpression(nameof(expectedSubstring))] string? expression = null)
where TException : Exception
{
source.Context.ExpressionBuilder.Append($".WithStackTraceContaining({expression})");
return new ExceptionStackTraceContainsAssertion<TException>(source.Context, expectedSubstring);
}

/// <summary>
/// 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&lt;Exception&gt;().And.WithStackTraceContaining("MyClass", StringComparison.OrdinalIgnoreCase);
/// </summary>
public static ExceptionStackTraceContainsAssertion<TException> WithStackTraceContaining<TException>(
this IAssertionSource<TException> 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<TException>(source.Context, expectedSubstring, comparison);
}

/// <summary>
/// Asserts that the exception has an inner exception of the specified type.
/// Works after Throws assertions.
/// Example: await Assert.That(() => ThrowingMethod()).Throws&lt;Exception&gt;().And.WithInnerException&lt;InvalidOperationException&gt;();
/// </summary>
public static ExceptionInnerExceptionOfTypeAssertion<TException, TInnerException> WithInnerException<TException, TInnerException>(
this IAssertionSource<TException> source)
where TException : Exception
where TInnerException : Exception
{
source.Context.ExpressionBuilder.Append($".WithInnerException<{typeof(TInnerException).Name}>()");
return new ExceptionInnerExceptionOfTypeAssertion<TException, TInnerException>(source.Context);
}

/// <summary>
/// Alias for WithMessage - asserts that the exception message exactly equals the specified string.
/// Works after Throws assertions.
/// Example: await Assert.That(() => ThrowingMethod()).Throws&lt;Exception&gt;().And.HasMessage("exact message");
/// </summary>
public static ExceptionMessageEqualsAssertion<TException> HasMessage<TException>(
this IAssertionSource<TException> source,
string expectedMessage,
[CallerArgumentExpression(nameof(expectedMessage))] string? expression = null)
where TException : Exception
{
return source.WithMessage(expectedMessage, expression);
}

/// <summary>
/// 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&lt;Exception&gt;().And.HasMessage("exact message", StringComparison.OrdinalIgnoreCase);
/// </summary>
public static ExceptionMessageEqualsAssertion<TException> HasMessage<TException>(
this IAssertionSource<TException> source,
string expectedMessage,
StringComparison comparison,
[CallerArgumentExpression(nameof(expectedMessage))] string? expression = null)
where TException : Exception
{
return source.WithMessage(expectedMessage, comparison, expression);
}

public static ThrowsAssertion<TException> Throws<TException>(this DelegateAssertion source) where TException : Exception
{
var iface = (IAssertionSource<object?>)source;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1089,6 +1089,14 @@ namespace .Conditions
protected override .<.> CheckAsync(.<TActual> metadata) { }
protected override string GetExpectation() { }
}
public class ExceptionInnerExceptionOfTypeAssertion<TException, TInnerException> : .<TException>
where TException :
where TInnerException :
{
public ExceptionInnerExceptionOfTypeAssertion(.<TException> context) { }
protected override .<.> CheckAsync(.<TException> metadata) { }
protected override string GetExpectation() { }
}
public class ExceptionMessageContainsAssertion<TException> : .<TException>
where TException :
{
Expand Down Expand Up @@ -1131,6 +1139,13 @@ namespace .Conditions
protected override .<.> CheckAsync(.<TException> metadata) { }
protected override string GetExpectation() { }
}
public class ExceptionStackTraceContainsAssertion<TException> : .<TException>
where TException :
{
public ExceptionStackTraceContainsAssertion(.<TException> context, string expectedSubstring, comparison = 4) { }
protected override .<.> CheckAsync(.<TException> metadata) { }
protected override string GetExpectation() { }
}
[.("IsNotExecutable")]
public class FileIsNotExecutableAssertion : .<.FileInfo>
{
Expand Down Expand Up @@ -1993,6 +2008,8 @@ namespace .Conditions
protected override bool CheckExceptionType( actualException, out string? errorMessage) { }
public .<?> WithExceptionType( expectedExceptionType) { }
public .<> WithInnerException() { }
public .<TException, TInnerException> WithInnerException<TInnerException>()
where TInnerException : { }
public .<TException> WithMessage(string expectedMessage) { }
public .<TException> WithMessage(string expectedMessage, comparison) { }
public .<TException> WithMessageContaining(string expectedSubstring) { }
Expand All @@ -2002,13 +2019,17 @@ namespace .Conditions
public .<TException> WithMessageNotContaining(string notExpectedSubstring) { }
public .<TException> WithMessageNotContaining(string notExpectedSubstring, comparison) { }
public .<TException> WithParameterName(string expectedParameterName) { }
public .<TException> WithStackTraceContaining(string expectedSubstring) { }
public .<TException> WithStackTraceContaining(string expectedSubstring, comparison) { }
}
public class ThrowsExactlyAssertion<TException> : .<TException, .<TException>>
where TException :
{
public ThrowsExactlyAssertion(.<TException> context) { }
protected override bool IsExactTypeMatch { get; }
protected override bool CheckExceptionType( actualException, out string? errorMessage) { }
public .<TException, TInnerException> WithInnerException<TInnerException>()
where TInnerException : { }
public .<TException> WithMessage(string expectedMessage) { }
public .<TException> WithMessage(string expectedMessage, comparison) { }
public .<TException> WithMessageContaining(string expectedSubstring) { }
Expand All @@ -2018,6 +2039,8 @@ namespace .Conditions
public .<TException> WithMessageNotContaining(string notExpectedSubstring) { }
public .<TException> WithMessageNotContaining(string notExpectedSubstring, comparison) { }
public .<TException> WithParameterName(string expectedParameterName) { }
public .<TException> WithStackTraceContaining(string expectedSubstring) { }
public .<TException> WithStackTraceContaining(string expectedSubstring, comparison) { }
}
public class ThrowsNothingAssertion<TValue> : .<TValue>
{
Expand Down Expand Up @@ -2497,6 +2520,10 @@ namespace .Extensions
public static ..LengthWrapper HasLength(this .<string> source) { }
[("Use Length().IsEqualTo(expectedLength) instead.")]
public static . HasLength(this .<string> source, int expectedLength, [.("expectedLength")] string? expression = null) { }
public static .<TException> HasMessage<TException>(this .<TException> source, string expectedMessage, [.("expectedMessage")] string? expression = null)
where TException : { }
public static .<TException> HasMessage<TException>(this .<TException> source, string expectedMessage, comparison, [.("expectedMessage")] string? expression = null)
where TException : { }
public static .<TException> HasMessageContaining<TException>(this .<TException> source, string expectedSubstring, [.("expectedSubstring")] string? expression = null)
where TException : { }
public static .<TException> HasMessageContaining<TException>(this .<TException> source, string expectedSubstring, comparison, [.("expectedSubstring")] string? expression = null)
Expand Down Expand Up @@ -2599,6 +2626,9 @@ namespace .Extensions
public static .<TValue> ThrowsNothing<TValue>(this .<TValue> source) { }
public static .<TValue> WaitsFor<TValue>(this .<TValue> source, <.<TValue>, .<TValue>> assertionBuilder, timeout, ? pollingInterval = default, [.("timeout")] string? timeoutExpression = null, [.("pollingInterval")] string? pollingIntervalExpression = null) { }
public static ..WhenParsedIntoAssertion<T> WhenParsedInto<[.(..None | ..PublicMethods | ..Interfaces)] T>(this .<string> source) { }
public static .<TException, TInnerException> WithInnerException<TException, TInnerException>(this .<TException> source)
where TException :
where TInnerException : { }
public static .<TException> WithMessage<TException>(this .<TException> source, string expectedMessage, [.("expectedMessage")] string? expression = null)
where TException : { }
public static .<TException> WithMessage<TException>(this .<TException> source, string expectedMessage, comparison, [.("expectedMessage")] string? expression = null)
Expand All @@ -2617,6 +2647,10 @@ namespace .Extensions
where TException : { }
public static .<TException> WithParameterName<TException>(this .<TException> source, string expectedParameterName, [.("expectedParameterName")] string? expression = null)
where TException : { }
public static .<TException> WithStackTraceContaining<TException>(this .<TException> source, string expectedSubstring, [.("expectedSubstring")] string? expression = null)
where TException : { }
public static .<TException> WithStackTraceContaining<TException>(this .<TException> source, string expectedSubstring, comparison, [.("expectedSubstring")] string? expression = null)
where TException : { }
}
public static class BetweenAssertionExtensions
{
Expand Down
Loading
Loading