Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
namespace TUnit.Assertions.Tests.Assertions.Delegates;

public partial class Throws
{
public class WithInnerExceptionsTests
{
[Test]
public async Task WithInnerExceptions_Count_Succeeds()
{
var aggregate = new AggregateException(
new InvalidOperationException("one"),
new ArgumentException("two"),
new FormatException("three"));
Action action = () => throw aggregate;

await Assert.That(action)
.Throws<AggregateException>()
.WithInnerExceptions(exceptions => exceptions.Count().IsEqualTo(3));
}

[Test]
public async Task WithInnerExceptions_Count_Fails()
{
var aggregate = new AggregateException(
new InvalidOperationException("one"),
new ArgumentException("two"));
Action action = () => throw aggregate;

var sut = async () => await Assert.That(action)
.Throws<AggregateException>()
.WithInnerExceptions(exceptions => exceptions.Count().IsEqualTo(5));

await Assert.That(sut).ThrowsException();
}

[Test]
public async Task WithInnerExceptions_AllSatisfy_Succeeds()
{
var aggregate = new AggregateException(
new ArgumentException("one", "param1"),
new ArgumentException("two", "param2"),
new ArgumentException("three", "param3"));
Action action = () => throw aggregate;

await Assert.That(action)
.Throws<AggregateException>()
.WithInnerExceptions(exceptions => exceptions
.All().Satisfy(e => e.IsTypeOf<ArgumentException>()));
}

[Test]
public async Task WithInnerExceptions_AllSatisfy_Fails_When_Mixed_Types()
{
var aggregate = new AggregateException(
new ArgumentException("one"),
new FormatException("two"));
Action action = () => throw aggregate;

var sut = async () => await Assert.That(action)
.Throws<AggregateException>()
.WithInnerExceptions(exceptions => exceptions
.All().Satisfy(e => e.IsTypeOf<ArgumentException>()));

await Assert.That(sut).ThrowsException();
}

[Test]
public async Task WithInnerExceptions_ThrowsExactly_Count_Succeeds()
{
var aggregate = new AggregateException(
new InvalidOperationException("one"),
new ArgumentException("two"));
Action action = () => throw aggregate;

await Assert.That(action)
.ThrowsExactly<AggregateException>()
.WithInnerExceptions(exceptions => exceptions.Count().IsEqualTo(2));
}

[Test]
public async Task WithInnerExceptions_Fails_When_Not_AggregateException()
{
Action action = () => throw new InvalidOperationException("not aggregate");

var sut = async () => await Assert.That(action)
.Throws<InvalidOperationException>()
.WithInnerExceptions(exceptions => exceptions.Count().IsEqualTo(1));

await Assert.That(sut).ThrowsException();
}
}
}
30 changes: 30 additions & 0 deletions TUnit.Assertions/Conditions/ThrowsAssertion.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Runtime.CompilerServices;
using TUnit.Assertions.Core;
using TUnit.Assertions.Sources;

namespace TUnit.Assertions.Conditions;

Expand Down Expand Up @@ -110,6 +112,20 @@ public ThrowsAssertion<Exception> WithInnerException()
return new ThrowsAssertion<Exception>(new AssertionContext<Exception>(innerExceptionContext, Context.ExpressionBuilder));
}

/// <summary>
/// Asserts on the InnerExceptions collection of an AggregateException using an inline assertion delegate.
/// The delegate receives a collection assertion source for InnerExceptions, enabling full collection
/// assertion chaining (Count, All().Satisfy, Contains, etc.).
/// Example: await Assert.That(action).Throws&lt;AggregateException&gt;().WithInnerExceptions(e => e.Count().IsEqualTo(3));
/// </summary>
public WithInnerExceptionsAssertion<TException> WithInnerExceptions(
Func<CollectionAssertion<Exception>, Assertion<IEnumerable<Exception>>?> innerExceptionsAssertion,
[CallerArgumentExpression(nameof(innerExceptionsAssertion))] string? expression = null)
{
Context.ExpressionBuilder.Append($".WithInnerExceptions({expression})");
return new WithInnerExceptionsAssertion<TException>(Context, innerExceptionsAssertion);
}

/// <summary>
/// Instance method for backward compatibility - delegates to extension method.
/// Asserts that the exception message contains the specified substring.
Expand Down Expand Up @@ -384,6 +400,20 @@ public ExceptionInnerExceptionOfTypeAssertion<TException, TInnerException> WithI
return new ExceptionInnerExceptionOfTypeAssertion<TException, TInnerException>(Context);
}

/// <summary>
/// Asserts on the InnerExceptions collection of an AggregateException using an inline assertion delegate.
/// The delegate receives a collection assertion source for InnerExceptions, enabling full collection
/// assertion chaining (Count, All().Satisfy, Contains, etc.).
/// Example: await Assert.That(action).ThrowsExactly&lt;AggregateException&gt;().WithInnerExceptions(e => e.Count().IsEqualTo(3));
/// </summary>
public WithInnerExceptionsAssertion<TException> WithInnerExceptions(
Func<CollectionAssertion<Exception>, Assertion<IEnumerable<Exception>>?> innerExceptionsAssertion,
[CallerArgumentExpression(nameof(innerExceptionsAssertion))] string? expression = null)
{
Context.ExpressionBuilder.Append($".WithInnerExceptions({expression})");
return new WithInnerExceptionsAssertion<TException>(Context, innerExceptionsAssertion);
}

/// <summary>
/// Asserts that the exception's stack trace contains the specified substring.
/// Example: await Assert.That(() => ThrowingMethod()).ThrowsExactly&lt;Exception&gt;().WithStackTraceContaining("MyClass.MyMethod");
Expand Down
57 changes: 57 additions & 0 deletions TUnit.Assertions/Conditions/WithInnerExceptionsAssertion.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using TUnit.Assertions.Core;
using TUnit.Assertions.Sources;

namespace TUnit.Assertions.Conditions;

/// <summary>
/// Asserts on the InnerExceptions collection of an AggregateException using an inline delegate.
/// The delegate receives a CollectionAssertion&lt;Exception&gt; for the InnerExceptions, enabling
/// full collection assertion chaining (Count, All().Satisfy, Contains, etc.).
/// </summary>
public class WithInnerExceptionsAssertion<TException> : Assertion<TException>
where TException : Exception
{
private readonly Func<CollectionAssertion<Exception>, Assertion<IEnumerable<Exception>>?> _innerExceptionsAssertion;

internal WithInnerExceptionsAssertion(
AssertionContext<TException> context,
Func<CollectionAssertion<Exception>, Assertion<IEnumerable<Exception>>?> innerExceptionsAssertion)
: base(context)
{
_innerExceptionsAssertion = innerExceptionsAssertion;
}

protected override async Task<AssertionResult> CheckAsync(EvaluationMetadata<TException> metadata)
{
var exception = metadata.Value;

if (exception is not AggregateException aggregateException)
{
return AssertionResult.Failed(
exception == null
? "exception was null"
: $"exception was {exception.GetType().Name}, not AggregateException");
}

var innerExceptions = aggregateException.InnerExceptions;
var collectionSource = new CollectionAssertion<Exception>(innerExceptions, "InnerExceptions");
var resultingAssertion = _innerExceptionsAssertion(collectionSource);

if (resultingAssertion != null)
{
try
{
await resultingAssertion.AssertAsync();
return AssertionResult.Passed;
}
catch
{
return AssertionResult.Failed("inner exceptions assertion failed");
}
}

return AssertionResult.Passed;
}

protected override string GetExpectation() => "to have inner exceptions satisfying assertion";
}
Original file line number Diff line number Diff line change
Expand Up @@ -2109,6 +2109,7 @@ namespace .Conditions
public .<> WithInnerException() { }
public .<TException, TInnerException> WithInnerException<TInnerException>()
where TInnerException : { }
public .<TException> WithInnerExceptions(<.<>, .<.<>>?> innerExceptionsAssertion, [.("innerExceptionsAssertion")] string? expression = null) { }
public .<TException> WithMessage(string expectedMessage) { }
public .<TException> WithMessage(string expectedMessage, comparison) { }
public .<TException> WithMessageContaining(string expectedSubstring) { }
Expand All @@ -2129,6 +2130,7 @@ namespace .Conditions
protected override bool CheckExceptionType( actualException, out string? errorMessage) { }
public .<TException, TInnerException> WithInnerException<TInnerException>()
where TInnerException : { }
public .<TException> WithInnerExceptions(<.<>, .<.<>>?> innerExceptionsAssertion, [.("innerExceptionsAssertion")] string? expression = null) { }
public .<TException> WithMessage(string expectedMessage) { }
public .<TException> WithMessage(string expectedMessage, comparison) { }
public .<TException> WithMessageContaining(string expectedSubstring) { }
Expand Down Expand Up @@ -2272,6 +2274,12 @@ namespace .Conditions
[.<>("TrackResurrection", CustomName="DoesNotTrackResurrection", ExpectationMessage="track resurrection", NegateLogic=true)]
[.<>("TrackResurrection", ExpectationMessage="track resurrection")]
public static class WeakReferenceAssertionExtensions { }
public class WithInnerExceptionsAssertion<TException> : .<TException>
where TException :
{
protected override .<.> CheckAsync(.<TException> metadata) { }
protected override string GetExpectation() { }
}
}
namespace .
{
Expand Down
Loading