diff --git a/TUnit.Assertions.Tests/CollectionAssertionTests.cs b/TUnit.Assertions.Tests/CollectionAssertionTests.cs index 1cc925828e..751e68dc5f 100644 --- a/TUnit.Assertions.Tests/CollectionAssertionTests.cs +++ b/TUnit.Assertions.Tests/CollectionAssertionTests.cs @@ -1,3 +1,6 @@ +using System.Diagnostics.CodeAnalysis; +using TUnit.Assertions.Extensions; + namespace TUnit.Assertions.Tests; public class CollectionAssertionTests @@ -33,4 +36,96 @@ public async Task Count2() await Assert.That(() => items).Count().IsEqualTo(0); } + + [Test] + [SuppressMessage("Obsolete", "CS0618:Type or member is obsolete")] + public async Task Count_WithPredicate() + { + var items = new List { 1, 2, 3, 4, 5 }; + +#pragma warning disable CS0618 // Type or member is obsolete + await Assert.That(items).Count(x => x > 2).IsEqualTo(3); +#pragma warning restore CS0618 + } + + [Test] + public async Task Count_WithInnerAssertion_IsGreaterThan() + { + var items = new List { 1, 2, 3, 4, 5 }; + + // Count items where item > 2 using inner assertion builder + await Assert.That(items).Count(item => item.IsGreaterThan(2)).IsEqualTo(3); + } + + [Test] + public async Task Count_WithInnerAssertion_IsLessThan() + { + var items = new List { 1, 2, 3, 4, 5 }; + + // Count items where item < 4 using inner assertion builder + await Assert.That(items).Count(item => item.IsLessThan(4)).IsEqualTo(3); + } + + [Test] + public async Task Count_WithInnerAssertion_IsBetween() + { + var items = new List { 1, 2, 3, 4, 5 }; + + // Count items where 2 <= item <= 4 using inner assertion builder + await Assert.That(items).Count(item => item.IsBetween(2, 4)).IsEqualTo(3); + } + + [Test] + public async Task Count_WithInnerAssertion_String_Contains() + { + var items = new List { "apple", "banana", "apricot", "cherry" }; + + // Count items that contain "ap" using inner assertion builder + await Assert.That(items).Count(item => item.Contains("ap")).IsEqualTo(2); + } + + [Test] + public async Task Count_WithInnerAssertion_String_StartsWith() + { + var items = new List { "apple", "banana", "apricot", "cherry" }; + + // Count items that start with "a" using inner assertion builder + await Assert.That(items).Count(item => item.StartsWith("a")).IsEqualTo(2); + } + + [Test] + public async Task Count_WithInnerAssertion_EmptyCollection() + { + var items = new List(); + + // Count on empty collection should return 0 + await Assert.That(items).Count(item => item.IsGreaterThan(0)).IsEqualTo(0); + } + + [Test] + public async Task Count_WithInnerAssertion_NoneMatch() + { + var items = new List { 1, 2, 3, 4, 5 }; + + // Count items > 10 (none match) + await Assert.That(items).Count(item => item.IsGreaterThan(10)).IsEqualTo(0); + } + + [Test] + public async Task Count_WithInnerAssertion_AllMatch() + { + var items = new List { 1, 2, 3, 4, 5 }; + + // Count items > 0 (all match) + await Assert.That(items).Count(item => item.IsGreaterThan(0)).IsEqualTo(5); + } + + [Test] + public async Task Count_WithInnerAssertion_Lambda_Collection() + { + var items = new List { 1, 2, 3, 4, 5 }; + + // Test with lambda-wrapped collection + await Assert.That(() => items).Count(item => item.IsGreaterThan(2)).IsEqualTo(3); + } } diff --git a/TUnit.Assertions/Conditions/CollectionCountValueAssertion.cs b/TUnit.Assertions/Conditions/CollectionCountValueAssertion.cs index 2b1c5a9670..c47ad6935f 100644 --- a/TUnit.Assertions/Conditions/CollectionCountValueAssertion.cs +++ b/TUnit.Assertions/Conditions/CollectionCountValueAssertion.cs @@ -1,5 +1,6 @@ using System.Collections; using TUnit.Assertions.Core; +using TUnit.Assertions.Sources; namespace TUnit.Assertions.Conditions; @@ -7,7 +8,7 @@ namespace TUnit.Assertions.Conditions; /// Assertion that evaluates the count of a collection and provides numeric assertions on that count. /// Implements IAssertionSource<int> to enable all numeric assertion methods. /// -public class CollectionCountValueAssertion : Sources.ValueAssertion +public class CollectionCountValueAssertion : ValueAssertion where TCollection : IEnumerable { public CollectionCountValueAssertion( @@ -42,3 +43,63 @@ private static AssertionContext CreateIntContext( }); } } + +/// +/// Assertion that evaluates the count of items satisfying an inner assertion and provides numeric assertions on that count. +/// Implements IAssertionSource<int> to enable all numeric assertion methods. +/// This allows using the full assertion builder (e.g., item => item.IsGreaterThan(10)) instead of a simple predicate. +/// +public class CollectionCountWithAssertionValueAssertion : ValueAssertion + where TCollection : IEnumerable +{ + public CollectionCountWithAssertionValueAssertion( + AssertionContext collectionContext, + Func, Assertion?> assertion) + : base(CreateIntContext(collectionContext, assertion)) + { + } + + private static AssertionContext CreateIntContext( + AssertionContext collectionContext, + Func, Assertion?> assertion) + { + return collectionContext.Map(async collection => + { + if (collection == null) + { + return 0; + } + + int count = 0; + int index = 0; + + foreach (var item in collection) + { + var itemAssertion = new ValueAssertion(item, $"item[{index}]"); + var resultingAssertion = assertion(itemAssertion); + + if (resultingAssertion != null) + { + try + { + await resultingAssertion.AssertAsync(); + count++; + } + catch + { + // Item did not satisfy the assertion, don't count it + } + } + else + { + // Null assertion means no constraint, count all items + count++; + } + + index++; + } + + return count; + }); + } +} diff --git a/TUnit.Assertions/Sources/CollectionAssertionBase.cs b/TUnit.Assertions/Sources/CollectionAssertionBase.cs index d6e0ee81f4..96964b17ec 100644 --- a/TUnit.Assertions/Sources/CollectionAssertionBase.cs +++ b/TUnit.Assertions/Sources/CollectionAssertionBase.cs @@ -146,6 +146,7 @@ public CollectionCountValueAssertion Count() /// This enables fluent assertions on filtered counts. /// Example: await Assert.That(list).Count(x => x > 10).IsEqualTo(3); /// + [Obsolete("Use Count(item => item.YourAssertion()) instead to leverage the full assertion builder. Example: Count(item => item.IsGreaterThan(10))")] public CollectionCountValueAssertion Count( Func predicate, [CallerArgumentExpression(nameof(predicate))] string? expression = null) @@ -154,6 +155,20 @@ public CollectionCountValueAssertion Count( return new CollectionCountValueAssertion(Context, predicate); } + /// + /// Gets the count of items satisfying the given assertion for further numeric assertions. + /// This enables fluent assertions on filtered counts using the full assertion builder. + /// Example: await Assert.That(list).Count(item => item.IsGreaterThan(10)).IsEqualTo(3); + /// + [OverloadResolutionPriority(1)] + public CollectionCountWithAssertionValueAssertion Count( + Func, Assertion?> assertion, + [CallerArgumentExpression(nameof(assertion))] string? expression = null) + { + Context.ExpressionBuilder.Append($".Count({expression})"); + return new CollectionCountWithAssertionValueAssertion(Context, assertion); + } + /// /// Asserts that the collection is ordered by the specified key selector in ascending order. /// This instance method enables calling IsOrderedBy with proper type inference. 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 e1e5f6bbf0..3fb14f4de1 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 @@ -426,6 +426,11 @@ namespace .Conditions { public CollectionCountValueAssertion(. collectionContext, ? predicate) { } } + public class CollectionCountWithAssertionValueAssertion : . + where TCollection : . + { + public CollectionCountWithAssertionValueAssertion(. collectionContext, <., .?> assertion) { } + } [.("DoesNotContain")] public class CollectionDoesNotContainAssertion : . where TCollection : . @@ -4366,7 +4371,11 @@ namespace .Sources public . Contains(TItem expected, [.("expected")] string? expression = null) { } public . ContainsOnly( predicate, [.("predicate")] string? expression = null) { } public . Count() { } + [("Use Count(item => ()) instead to leverage the full assertion bu" + + "ilder. Example: Count(item => (10))")] public . Count( predicate, [.("predicate")] string? expression = null) { } + [.(1)] + public . Count(<., .?> assertion, [.("assertion")] string? expression = null) { } public . DoesNotContain( predicate, [.("predicate")] string? expression = null) { } public . DoesNotContain(TItem expected, [.("expected")] string? expression = null) { } protected override string GetExpectation() { } 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 d5b2de52a2..af9658c1fa 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 @@ -421,6 +421,11 @@ namespace .Conditions { public CollectionCountValueAssertion(. collectionContext, ? predicate) { } } + public class CollectionCountWithAssertionValueAssertion : . + where TCollection : . + { + public CollectionCountWithAssertionValueAssertion(. collectionContext, <., .?> assertion) { } + } [.("DoesNotContain")] public class CollectionDoesNotContainAssertion : . where TCollection : . @@ -4344,7 +4349,10 @@ namespace .Sources public . Contains(TItem expected, [.("expected")] string? expression = null) { } public . ContainsOnly( predicate, [.("predicate")] string? expression = null) { } public . Count() { } + [("Use Count(item => ()) instead to leverage the full assertion bu" + + "ilder. Example: Count(item => (10))")] public . Count( predicate, [.("predicate")] string? expression = null) { } + public . Count(<., .?> assertion, [.("assertion")] string? expression = null) { } public . DoesNotContain( predicate, [.("predicate")] string? expression = null) { } public . DoesNotContain(TItem expected, [.("expected")] string? expression = null) { } protected override string GetExpectation() { } 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 a6c9c27621..66d5d3d282 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 @@ -426,6 +426,11 @@ namespace .Conditions { public CollectionCountValueAssertion(. collectionContext, ? predicate) { } } + public class CollectionCountWithAssertionValueAssertion : . + where TCollection : . + { + public CollectionCountWithAssertionValueAssertion(. collectionContext, <., .?> assertion) { } + } [.("DoesNotContain")] public class CollectionDoesNotContainAssertion : . where TCollection : . @@ -4366,7 +4371,11 @@ namespace .Sources public . Contains(TItem expected, [.("expected")] string? expression = null) { } public . ContainsOnly( predicate, [.("predicate")] string? expression = null) { } public . Count() { } + [("Use Count(item => ()) instead to leverage the full assertion bu" + + "ilder. Example: Count(item => (10))")] public . Count( predicate, [.("predicate")] string? expression = null) { } + [.(1)] + public . Count(<., .?> assertion, [.("assertion")] string? expression = null) { } public . DoesNotContain( predicate, [.("predicate")] string? expression = null) { } public . DoesNotContain(TItem expected, [.("expected")] string? expression = null) { } protected override string GetExpectation() { } 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 6e579544ed..0b1c05b81b 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 @@ -417,6 +417,11 @@ namespace .Conditions { public CollectionCountValueAssertion(. collectionContext, ? predicate) { } } + public class CollectionCountWithAssertionValueAssertion : . + where TCollection : . + { + public CollectionCountWithAssertionValueAssertion(. collectionContext, <., .?> assertion) { } + } [.("DoesNotContain")] public class CollectionDoesNotContainAssertion : . where TCollection : . @@ -3833,7 +3838,10 @@ namespace .Sources public . Contains(TItem expected, [.("expected")] string? expression = null) { } public . ContainsOnly( predicate, [.("predicate")] string? expression = null) { } public . Count() { } + [("Use Count(item => ()) instead to leverage the full assertion bu" + + "ilder. Example: Count(item => (10))")] public . Count( predicate, [.("predicate")] string? expression = null) { } + public . Count(<., .?> assertion, [.("assertion")] string? expression = null) { } public . DoesNotContain( predicate, [.("predicate")] string? expression = null) { } public . DoesNotContain(TItem expected, [.("expected")] string? expression = null) { } protected override string GetExpectation() { } diff --git a/docs/docs/assertions/collections.md b/docs/docs/assertions/collections.md index db49665263..f35d0f9a52 100644 --- a/docs/docs/assertions/collections.md +++ b/docs/docs/assertions/collections.md @@ -131,21 +131,36 @@ public async Task Count_With_Comparison() } ``` -### Count with Predicate +### Count with Inner Assertion -Count items matching a predicate: +Count items that satisfy an assertion, allowing you to reuse existing assertion methods: ```csharp [Test] -public async Task Count_With_Predicate() +public async Task Count_With_Inner_Assertion() { var numbers = new[] { 1, 2, 3, 4, 5, 6 }; - // Count even numbers - var evenCount = await Assert.That(numbers) - .Count(n => n % 2 == 0); + // Count numbers greater than 3 using assertion builder + await Assert.That(numbers) + .Count(item => item.IsGreaterThan(3)) + .IsEqualTo(3); + + // Count numbers between 2 and 5 + await Assert.That(numbers) + .Count(item => item.IsBetween(2, 5)) + .IsEqualTo(4); +} + +[Test] +public async Task Count_Strings_With_Inner_Assertion() +{ + var names = new[] { "Alice", "Bob", "Andrew", "Charlie" }; - await Assert.That(evenCount).IsEqualTo(3); + // Count names starting with "A" + await Assert.That(names) + .Count(item => item.StartsWith("A")) + .IsEqualTo(2); } ``` @@ -490,7 +505,7 @@ The default behavior (ignoring order) is ideal for: public async Task Database_Query_Results() { var results = await database.GetActiveUsersAsync(); - + // Order doesn't matter for this assertion await Assert.That(results) .IsEquivalentTo(new[] { user1, user2, user3 }); @@ -510,7 +525,7 @@ Use order-sensitive comparison when: public async Task Sorted_Query_Results() { var results = await database.GetUsersSortedByNameAsync(); - + // Order matters here await Assert.That(results) .IsEquivalentTo( @@ -530,7 +545,7 @@ public async Task Multiple_Order_Sensitive_Checks() { var list1 = GetSortedList1(); var list2 = GetSortedList2(); - + // Be explicit about ordering requirements await Assert.That(list1).IsEquivalentTo(expected1, CollectionOrdering.Matching); await Assert.That(list2).IsEquivalentTo(expected2, CollectionOrdering.Matching); @@ -544,7 +559,7 @@ For ordered comparisons, you can also use `IsInOrder()`: public async Task Verify_Ordering_Separately() { var actual = new[] { 1, 2, 3 }; - + // Check both equivalency and ordering await Assert.That(actual).IsEquivalentTo(new[] { 1, 2, 3 }); await Assert.That(actual).IsInOrder();