From 8b41d02f96e8ccc72596e93b60e9d41f17ebbd2b Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Thu, 16 Oct 2025 20:29:50 +0100 Subject: [PATCH] refactor(assertions): enhance collection assertion types for improved type inference and chaining --- TUnit.Assertions.Tests/TypeInferenceTests.cs | 13 ++-- .../Conditions/CollectionAssertions.cs | 62 ++++++++++++------- ...Has_No_API_Changes.DotNet10_0.verified.txt | 5 +- ..._Has_No_API_Changes.DotNet8_0.verified.txt | 5 +- ..._Has_No_API_Changes.DotNet9_0.verified.txt | 5 +- ...ary_Has_No_API_Changes.Net4_7.verified.txt | 5 +- 6 files changed, 61 insertions(+), 34 deletions(-) diff --git a/TUnit.Assertions.Tests/TypeInferenceTests.cs b/TUnit.Assertions.Tests/TypeInferenceTests.cs index 2b13770904..b4e0d9b5b7 100644 --- a/TUnit.Assertions.Tests/TypeInferenceTests.cs +++ b/TUnit.Assertions.Tests/TypeInferenceTests.cs @@ -36,16 +36,21 @@ await Assert.That(enumerable) [Test] public async Task PredicateAssertionsReturnItem() { - // Contains with predicate returns the found item, not the collection - // This allows further assertions on the item itself + // Contains with predicate can be awaited to get the found item + // Or chained with .And to continue collection assertions IEnumerable enumerable = [1, 2, 3]; try { + // Test 1: Await to get the found item + var item = await Assert.That(enumerable).Contains(x => x > 1); + await Assert.That(item).IsGreaterThan(0); + + // Test 2: Chain with .And for collection assertions await Assert.That(enumerable) - .Contains(x => x > 1) // Returns Assertion with the found item + .Contains(x => x > 1) .And - .IsGreaterThan(0); // Can assert on the found item + .Contains(x => x > 2); // Can chain multiple Contains } catch { diff --git a/TUnit.Assertions/Conditions/CollectionAssertions.cs b/TUnit.Assertions/Conditions/CollectionAssertions.cs index 9a93232fb4..c3c39802df 100644 --- a/TUnit.Assertions/Conditions/CollectionAssertions.cs +++ b/TUnit.Assertions/Conditions/CollectionAssertions.cs @@ -531,51 +531,69 @@ protected override Task CheckAsync(EvaluationMetadata m /// /// Asserts that a collection contains an item matching the predicate. +/// When awaited, returns the found item for further assertions. /// -public class CollectionContainsPredicateAssertion : Assertion +public class CollectionContainsPredicateAssertion : Assertion where TCollection : IEnumerable { private readonly Func _predicate; + private TItem? _foundItem; public CollectionContainsPredicateAssertion( AssertionContext context, Func predicate) - : base(context.Map(collection => - { - if (collection == null) - { - throw new ArgumentNullException(nameof(collection), "collection was null"); - } - - foreach (var item in collection) - { - if (predicate(item)) - { - return item; - } - } - - throw new InvalidOperationException("no item matching predicate found in collection"); - })) + : base(context) { _predicate = predicate ?? throw new ArgumentNullException(nameof(predicate)); } - protected override Task CheckAsync(EvaluationMetadata metadata) + protected override Task CheckAsync(EvaluationMetadata metadata) { var value = metadata.Value; var exception = metadata.Exception; if (exception != null) { - return Task.FromResult(AssertionResult.Failed(exception.Message)); + return Task.FromResult(AssertionResult.Failed($"threw {exception.GetType().Name}")); } - // If we got here, the item was found (the Map function succeeded) - return Task.FromResult(AssertionResult.Passed); + if (value == null) + { + return Task.FromResult(AssertionResult.Failed("collection was null")); + } + + // Search for matching item + foreach (var item in value) + { + if (_predicate(item)) + { + _foundItem = item; + return Task.FromResult(AssertionResult.Passed); + } + } + + return Task.FromResult(AssertionResult.Failed("no item matching predicate found in collection")); } protected override string GetExpectation() => "to contain item matching predicate"; + + /// + /// Enables await syntax that returns the found item. + /// This allows both chaining (.And) and item capture (await). + /// + public new System.Runtime.CompilerServices.TaskAwaiter GetAwaiter() + { + return ExecuteAndReturnItemAsync().GetAwaiter(); + } + + private async Task ExecuteAndReturnItemAsync() + { + // Execute the assertion (will throw if item not found) + await AssertAsync(); + + // Return the found item + return _foundItem!; + } } /// diff --git a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt index 4335a9ca3f..4db3402331 100644 --- a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt @@ -384,11 +384,12 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } - public class CollectionContainsPredicateAssertion : . + public class CollectionContainsPredicateAssertion : . where TCollection : . { public CollectionContainsPredicateAssertion(. context, predicate) { } - protected override .<.> CheckAsync(. metadata) { } + protected override .<.> CheckAsync(. metadata) { } + public new . GetAwaiter() { } protected override string GetExpectation() { } } public class CollectionCountAssertion : . diff --git a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt index de0cee0ce3..0ff974d420 100644 --- a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt @@ -384,11 +384,12 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } - public class CollectionContainsPredicateAssertion : . + public class CollectionContainsPredicateAssertion : . where TCollection : . { public CollectionContainsPredicateAssertion(. context, predicate) { } - protected override .<.> CheckAsync(. metadata) { } + protected override .<.> CheckAsync(. metadata) { } + public new . GetAwaiter() { } protected override string GetExpectation() { } } public class CollectionCountAssertion : . diff --git a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt index 855f6d123e..96d2cb7b52 100644 --- a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt @@ -384,11 +384,12 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } - public class CollectionContainsPredicateAssertion : . + public class CollectionContainsPredicateAssertion : . where TCollection : . { public CollectionContainsPredicateAssertion(. context, predicate) { } - protected override .<.> CheckAsync(. metadata) { } + protected override .<.> CheckAsync(. metadata) { } + public new . GetAwaiter() { } protected override string GetExpectation() { } } public class CollectionCountAssertion : . diff --git a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt index f599c3b06d..e3edd02d56 100644 --- a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt +++ b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt @@ -382,11 +382,12 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } - public class CollectionContainsPredicateAssertion : . + public class CollectionContainsPredicateAssertion : . where TCollection : . { public CollectionContainsPredicateAssertion(. context, predicate) { } - protected override .<.> CheckAsync(. metadata) { } + protected override .<.> CheckAsync(. metadata) { } + public new . GetAwaiter() { } protected override string GetExpectation() { } } public class CollectionCountAssertion : .