From b3c68686635c408ea391b9960bf3a06d25bdd5c8 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Mon, 3 Aug 2026 04:08:34 -0700 Subject: [PATCH] fix: support nested Member after IsTypeOf Fixes #6528 --- .../Conditions/MemberAssertion.cs | 9 ++- .../Extensions/AssertionExtensions.cs | 5 ++ .../Bugs/Issue6528Tests.cs | 56 +++++++++++++++++++ 3 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 tests/TUnit.Assertions.Tests/Bugs/Issue6528Tests.cs diff --git a/src/TUnit.Assertions/Conditions/MemberAssertion.cs b/src/TUnit.Assertions/Conditions/MemberAssertion.cs index b8ae95b528d..e5707f816fd 100644 --- a/src/TUnit.Assertions/Conditions/MemberAssertion.cs +++ b/src/TUnit.Assertions/Conditions/MemberAssertion.cs @@ -4,12 +4,17 @@ namespace TUnit.Assertions.Conditions; +internal interface ITypeErasedMemberAssertion +{ + Assertion TypeErasedAssertion { get; } +} + /// /// 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. /// -public class MemberAssertionResult : IAssertion +public class MemberAssertionResult : IAssertion, ITypeErasedMemberAssertion { private readonly AssertionContext _parentContext; private readonly Assertion _memberAssertion; @@ -29,6 +34,8 @@ async Task IAssertion.AssertAsync() await _memberAssertion.AssertAsync(); } + Assertion ITypeErasedMemberAssertion.TypeErasedAssertion => _memberAssertion; + /// /// Returns an And continuation that operates on the parent object's context, /// allowing chaining of multiple member assertions on the same parent object. diff --git a/src/TUnit.Assertions/Extensions/AssertionExtensions.cs b/src/TUnit.Assertions/Extensions/AssertionExtensions.cs index 7041484a623..f30b71690dc 100644 --- a/src/TUnit.Assertions/Extensions/AssertionExtensions.cs +++ b/src/TUnit.Assertions/Extensions/AssertionExtensions.cs @@ -828,6 +828,11 @@ public static MemberAssertionResult Member( 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 base class diff --git a/tests/TUnit.Assertions.Tests/Bugs/Issue6528Tests.cs b/tests/TUnit.Assertions.Tests/Bugs/Issue6528Tests.cs new file mode 100644 index 00000000000..3ca08ee5b16 --- /dev/null +++ b/tests/TUnit.Assertions.Tests/Bugs/Issue6528Tests.cs @@ -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() + .And.Member( + exception => exception.InnerException, + assertion => assertion.IsTypeOf() + .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(async () => + { + await Assert.That(action) + .ThrowsExactly() + .And.Member( + exception => exception.InnerException, + assertion => assertion.IsTypeOf() + .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(async () => + { + await Assert.That(action) + .ThrowsExactly() + .And.Member( + exception => exception.InnerException, + assertion => assertion.IsTypeOf() + .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; + } +}