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
9 changes: 8 additions & 1 deletion src/TUnit.Assertions/Conditions/MemberAssertion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@

namespace TUnit.Assertions.Conditions;

internal interface ITypeErasedMemberAssertion
{
Assertion<object?> TypeErasedAssertion { get; }
}

/// <summary>
/// Result of a member assertion that allows returning to the parent object context.
/// Enables chaining multiple member assertions on the same parent object.
/// Implements IAssertion to allow use in Satisfies() lambdas that accept IAssertion.
/// </summary>
public class MemberAssertionResult<TObject> : IAssertion
public class MemberAssertionResult<TObject> : IAssertion, ITypeErasedMemberAssertion
{
private readonly AssertionContext<TObject> _parentContext;
private readonly Assertion<object?> _memberAssertion;
Expand All @@ -29,6 +34,8 @@ async Task IAssertion.AssertAsync()
await _memberAssertion.AssertAsync();
}

Assertion<object?> ITypeErasedMemberAssertion.TypeErasedAssertion => _memberAssertion;

/// <summary>
/// Returns an And continuation that operates on the parent object's context,
/// allowing chaining of multiple member assertions on the same parent object.
Expand Down
5 changes: 5 additions & 0 deletions src/TUnit.Assertions/Extensions/AssertionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -828,6 +828,11 @@ public static MemberAssertionResult<TObject> Member<TObject, TMember>(
throw new InvalidOperationException("Member assertion cannot be null.");
}

if (memberAssertion is ITypeErasedMemberAssertion typeErasedMemberAssertion)
{
return typeErasedMemberAssertion.TypeErasedAssertion;
}

var type = memberAssertion.GetType();

// Walk up the inheritance chain to find the Assertion<T> base class
Expand Down
56 changes: 56 additions & 0 deletions tests/TUnit.Assertions.Tests/Bugs/Issue6528Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
namespace TUnit.Assertions.Tests.Bugs;

public class Issue6528Tests
{
[Test]
public async Task Nested_Member_After_IsTypeOf_Succeeds()
{
Action action = () => throw new OuterEx(new InnerEx(5));

await Assert.That(action)
.ThrowsExactly<OuterEx>()
.And.Member(
exception => exception.InnerException,
assertion => assertion.IsTypeOf<InnerEx>()
.And.Member(inner => inner.Code, code => code.IsEqualTo(5)));
}

[Test]
public async Task Nested_Member_After_IsTypeOf_Executes_Nested_Assertion()
{
Action action = () => throw new OuterEx(new InnerEx(4));

await Assert.ThrowsAsync<AssertionException>(async () =>
{
await Assert.That(action)
.ThrowsExactly<OuterEx>()
.And.Member(
exception => exception.InnerException,
assertion => assertion.IsTypeOf<InnerEx>()
.And.Member(inner => inner.Code, code => code.IsEqualTo(5)));
});
}

[Test]
public async Task Nested_Member_After_IsTypeOf_Executes_Type_Assertion()
{
Action action = () => throw new OuterEx(new InvalidOperationException());

await Assert.ThrowsAsync<AssertionException>(async () =>
{
await Assert.That(action)
.ThrowsExactly<OuterEx>()
.And.Member(
exception => exception.InnerException,
assertion => assertion.IsTypeOf<InnerEx>()
.And.Member(inner => inner.Code, code => code.IsEqualTo(5)));
});
}

private sealed class OuterEx(Exception innerException) : Exception("Outer exception", innerException);

private sealed class InnerEx(int code) : Exception
{
public int Code { get; } = code;
}
}
Loading