feat: add string length range assertions#4935
Conversation
Code ReviewThe implementation follows the established patterns in Two issues found: 1. Missing Public API snapshot updates (CLAUDE.md violation)Severity: Required
The analogous existing type
Fix: Run the # From the TUnit.PublicAPI directory
dotnet test
# Then accept the output:
for f in *.received.txt; do mv "$f" "${f%.received.txt}.verified.txt"; done
git add *.verified.txt2.
|
b950983 to
28ce284
Compare
28ce284 to
e23c286
Compare
e23c286 to
cf5f7b1
Compare
cf5f7b1 to
c10dc39
Compare
1401303 to
cf3d677
Compare
There was a problem hiding this comment.
Code Review: Add String Length Range Assertions
Summary
This PR adds three new string assertions:
HasMinLength(int minLength)— passes whenstring.Length >= minLengthHasMaxLength(int maxLength)— passes whenstring.Length <= maxLengthHasLengthBetween(int minLength, int maxLength)— passes when length is within an inclusive range
The implementation correctly follows the [GenerateAssertion] source generator pattern. ~380 of 512 changed lines are unrelated snapshot updates for a separate covariance improvement to exception assertions.
Previous Review Comments
The prior review flagged two issues: null handling and missing tests.
- Null handling — the criticism was a false positive. The
MethodAssertionGeneratorautomatically injects a null guard for all reference types, so condition methods never receive null. The null tests are still valuable regression coverage. - Tests — fully addressed with
StringLengthAssertionTests.cs(30 tests added).
Issues Found
1. Missing source generator snapshot tests (medium priority)
The project has snapshot tests that verify exact generated code for each [GenerateAssertion]-decorated class (e.g., AssertionMethodGeneratorTests.GeneratesStringAssertions.*.verified.txt). The PR doesn't add corresponding snapshot tests for StringLengthRangeAssertionExtensions. This means the exact shape of the generated String_HasMinLength_Int_Assertion, String_HasMaxLength_Int_Assertion, and String_HasLengthBetween_Int_Int_Assertion classes is unverified — a regression in the generator would go undetected.
2. Unrelated covariance change bundled in (low priority)
About 380 of 512 changed lines are snapshot updates for a separate feature: making exception assertion extension methods covariant using <TActual> where TActual : TException. The covariance change itself looks correct, but mixing it here reduces reviewability and commit history clarity. Ideally this would be a separate PR.
3. Test file scope (minor)
Roughly half the tests in StringLengthAssertionTests.cs test the existing Length() numeric chain API (Length_IsEqualTo_*, Length_IsGreaterThan_*, etc.), not the new assertions. These belong in a pre-existing test file rather than one named after the new feature. They inflate the diff without testing what this PR adds.
4. Design note: HasLengthBetween validation timing
// StringLengthRangeAssertionExtensions.cs
public static AssertionResult HasLengthBetween(this string value, int minLength, int maxLength)
{
if (minLength > maxLength)
throw new ArgumentOutOfRangeException(nameof(minLength), ...);
...
}The ArgumentOutOfRangeException test works because the exception propagates out of the generated CheckAsync, which the test wraps in Assert.That(async () => ...).Throws<ArgumentOutOfRangeException>(). This is functionally correct, but a programming mistake (min > max) surfaces as a runtime exception only when the assertion is awaited.
A more defensive alternative would be to validate at construction time — in the generated extension method rather than inside the condition method. However, with the current generator architecture this isn't possible without hand-authoring. Not a blocking issue; just worth noting that the guard is assertion-await-dependent.
What's Done Well
- Correct use of
[GenerateAssertion]pattern —[EditorBrowsable(EditorBrowsableState.Never)]is present,ExpectationMessageis provided for all three methods. - Null safety is handled correctly by the generator's automatic null guard injection.
- Public API snapshots updated — all 4 target framework variants (net4.7, net8, net9, net10) have updated
.verified.txtfiles. ArgumentOutOfRangeExceptionguard inHasLengthBetween— prevents nonsensical range assertions.
Overall
The core feature is correct, well-implemented, and follows project conventions. The main recommendation is to add source generator snapshot tests for the new assertion classes. The null handling that was flagged in the previous review is handled correctly by the generator infrastructure — no changes needed there.
Updated [TUnit](https://github.com/thomhurst/TUnit) from 1.21.6 to 1.21.20. <details> <summary>Release notes</summary> _Sourced from [TUnit's releases](https://github.com/thomhurst/TUnit/releases)._ ## 1.21.20 <!-- Release notes generated using configuration in .github/release.yml at v1.21.20 --> ## What's Changed ### Other Changes * fix: respect TUnitImplicitUsings set in Directory.Build.props by @thomhurst in thomhurst/TUnit#5225 * feat: covariant assertions for interfaces and non-sealed classes by @thomhurst in thomhurst/TUnit#5226 * feat: support string-to-parseable type conversions in [Arguments] by @thomhurst in thomhurst/TUnit#5227 * feat: add string length range assertions by @thomhurst in thomhurst/TUnit#4935 * Fix BeforeEvery/AfterEvery hooks for Class and Assembly not being executed by @Copilot in thomhurst/TUnit#5239 ### Dependencies * chore(deps): update tunit to 1.21.6 by @thomhurst in thomhurst/TUnit#5228 * chore(deps): update dependency gitversion.msbuild to 6.7.0 by @thomhurst in thomhurst/TUnit#5229 * chore(deps): update dependency gitversion.tool to v6.7.0 by @thomhurst in thomhurst/TUnit#5230 * chore(deps): update aspire to 13.2.0 - autoclosed by @thomhurst in thomhurst/TUnit#5232 * chore(deps): update dependency typescript to v6 by @thomhurst in thomhurst/TUnit#5233 * chore(deps): update dependency polyfill to 9.23.0 by @thomhurst in thomhurst/TUnit#5235 * chore(deps): update dependency polyfill to 9.23.0 by @thomhurst in thomhurst/TUnit#5236 **Full Changelog**: thomhurst/TUnit@v1.21.6...v1.21.20 Commits viewable in [compare view](thomhurst/TUnit@v1.21.6...v1.21.20). </details> [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) </details> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Summary
HasMinLength(int minLength)assertion that verifies a string's length is >= the specified minimumHasMaxLength(int maxLength)assertion that verifies a string's length is <= the specified maximumHasLengthBetween(int min, int max)assertion that verifies a string's length is within an inclusive rangeAll three follow the existing assertion patterns (condition classes in
StringAssertions.cs, extension methods inAssertionExtensions.cs) and supportCallerArgumentExpressionfor readable error messages.Closes #4868
Test plan
HasMinLengthpasses for strings at or above the minimum lengthHasMinLengthfails for strings below the minimum lengthHasMaxLengthpasses for strings at or below the maximum lengthHasMaxLengthfails for strings above the maximum lengthHasLengthBetweenpasses for strings within the inclusive rangeHasLengthBetweenfails for strings outside the rangeAssert.That(str).HasMinLength(3).And.HasMaxLength(10))