-
-
Notifications
You must be signed in to change notification settings - Fork 109
feat: add Count method with inner assertion support for collections #4041
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Pull Request Review: Count with Inner Assertion SupportThank you for this feature addition! This is a nice enhancement to the assertion API that provides better expressiveness. Positives
Issues and Concerns1. CRITICAL: Catch All Exceptions (CollectionCountValueAssertion.cs:83-91)The bare catch block is an anti-pattern explicitly mentioned in CLAUDE.md. Catching all exceptions without type specification silently swallows critical exceptions like OutOfMemoryException, StackOverflowException, and ThreadAbortException. Recommendation: Replace with specific exception type that assertions throw when they fail. 2. Performance: Allocations in Hot Path (CollectionCountValueAssertion.cs:76-100)The foreach loop creates allocations on every iteration:
Impact: For 10,000 items, this creates 30,000+ allocations. 3. Documentation: Missing Performance NotesThe XML documentation should mention O(n) complexity and suggest using the simple predicate overload for better performance. 4. Testing: Missing Failure Case TestsConsider adding tests for assertion failure scenarios, complex nested assertions, and null item handling. Action ItemsMust Fix Before Merge:
Should Fix: Nice to Have: SummarySolid feature addition with good test coverage. Main concerns are the catch-all exception handler (critical fix needed) and performance implications in hot paths. Overall Assessment: Needs Changes (primarily exception handling fix) |
Code Review: Add Count method with inner assertion supportSummaryThis PR adds a powerful new API for counting collection items using the full assertion builder instead of simple predicates. The implementation is well-designed and follows TUnit's patterns, but there are several important concerns to address. ✅ Strengths
🚨 Critical Issues1. Performance - Exception-Based Control Flow (CRITICAL)Location: The implementation uses exceptions for control flow, which violates TUnit's performance-first principle: try
{
await resultingAssertion.AssertAsync();
count++;
}
catch
{
// Item did not satisfy the assertion, don't count it
}Problem: Exceptions are extremely expensive in .NET (~1000x slower than normal control flow). For large collections, this could cause significant performance degradation. Impact: Users counting items in collections with thousands of elements will see severe slowdowns, especially when many items don't match. Suggested Fix: The assertion framework should expose a // Option 1: Check if assertion passes without executing
var result = await resultingAssertion.GetResultAsync(); // Returns AssertionResult
if (result.Passed)
{
count++;
}
// Option 2: If no alternative exists, document the performance implications
// and consider adding a fast-path for common assertions2. Overly Broad Exception CatchingLocation: catch
{
// Item did not satisfy the assertion, don't count it
}Problem: Catching all exceptions swallows important errors:
Fix: At minimum, catch only catch (AssertionException)
{
// Item did not satisfy the assertion, don't count it
}
|
| Category | Rating | Notes |
|---|---|---|
| API Design | ⭐⭐⭐⭐⭐ | Excellent, intuitive API |
| Test Coverage | ⭐⭐⭐⭐ | Comprehensive edge cases |
| Performance | ⭐⭐ | Exception-based control flow is a concern |
| Code Style | ⭐⭐⭐⭐⭐ | Follows TUnit conventions |
| Documentation | ⭐⭐⭐⭐ | Well documented |
Conclusion
This is a valuable feature with great API design. The main blocker is the exception-based control flow which could cause performance issues in production. Please investigate if the assertion framework provides a non-throwing API for checking assertion results. If not, document the performance implications clearly.
Status:
Great work overall! The feature itself is excellent—just needs some implementation refinements.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds a new overload for the Count() method on collections that accepts an assertion builder function instead of a simple predicate. This enables more expressive filtering using the full TUnit assertion API (e.g., Count(item => item.IsGreaterThan(10))) rather than just predicates.
Key Changes
- New
CollectionCountWithAssertionValueAssertion<TCollection, TItem>class that evaluates assertions on collection items - New
Count()overload inCollectionAssertionBasemarked withOverloadResolutionPriority(1)to disambiguate from the predicate overload - Comprehensive test coverage for various scenarios including empty collections, no matches, all matches, and different data types
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt |
Updated API snapshot to include new Count overload and CollectionCountWithAssertionValueAssertion class |
TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt |
Updated API snapshot with OverloadResolutionPriority attribute annotation for .NET 9.0 |
TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt |
Updated API snapshot for .NET 8.0 compatibility |
TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt |
Updated API snapshot with OverloadResolutionPriority attribute for .NET 10.0 |
TUnit.Assertions/Sources/CollectionAssertionBase.cs |
Added new Count overload accepting assertion builder with appropriate documentation and overload resolution priority |
TUnit.Assertions/Conditions/CollectionCountValueAssertion.cs |
Implemented CollectionCountWithAssertionValueAssertion class that counts items satisfying inner assertions |
TUnit.Assertions.Tests/CollectionAssertionTests.cs |
Added comprehensive tests covering various scenarios for the new Count overload |
|
|
||
| foreach (var item in collection) | ||
| { | ||
| var itemAssertion = new ValueAssertion<TItem>(item, $"item[{index}]"); |
Copilot
AI
Dec 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
String interpolation in a loop creates a new string for each iteration. Since this expression is only used for potential error messages in the inner assertion and not for the count logic itself, consider if this allocation is necessary in a hot path.
If the expression isn't critical for this use case, you might pass null or a constant string to avoid repeated allocations. Alternatively, use a cached format if the expression is needed.
| var itemAssertion = new ValueAssertion<TItem>(item, $"item[{index}]"); | |
| var itemAssertion = new ValueAssertion<TItem>(item, null); |
| try | ||
| { | ||
| await resultingAssertion.AssertAsync(); | ||
| count++; | ||
| } | ||
| catch | ||
| { | ||
| // Item did not satisfy the assertion, don't count it | ||
| } |
Copilot
AI
Dec 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using exceptions for control flow is a performance anti-pattern. When items don't satisfy the assertion, an AssertionException is thrown and caught for each non-matching item. Exception creation and stack trace generation is expensive.
Consider checking if Assertion<TItem> provides a non-throwing method to evaluate the assertion result, similar to patterns used elsewhere in the codebase with CheckAsync returning AssertionResult. If not available, document this performance characteristic or consider an alternative approach that doesn't rely on exception handling for expected control flow.
| try | |
| { | |
| await resultingAssertion.AssertAsync(); | |
| count++; | |
| } | |
| catch | |
| { | |
| // Item did not satisfy the assertion, don't count it | |
| } | |
| var result = await resultingAssertion.CheckAsync(); | |
| if (result.IsPassed) | |
| { | |
| count++; | |
| } | |
| // If not passed, don't count it |
| await resultingAssertion.AssertAsync(); | ||
| count++; | ||
| } | ||
| catch |
Copilot
AI
Dec 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bare catch blocks without exception type specification should be avoided as they catch all exceptions, including system-critical ones like OutOfMemoryException, StackOverflowException, and ThreadAbortException. This can hide unexpected bugs and make debugging difficult.
Change to catch (AssertionException) to specifically catch only assertion failures, which is the expected exception type from AssertAsync(). Other exception types (e.g., NullReferenceException, InvalidOperationException) likely indicate bugs that should not be silently ignored.
No description provided.