From ea3363d4cb799f9f6d4974c13975aabc4dc60d3a Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Thu, 19 Feb 2026 04:00:07 +0000 Subject: [PATCH 1/4] feat: add string length range assertions (HasMinLength, HasMaxLength, HasLengthBetween) Add three new string assertion methods for validating string length ranges: - HasMinLength(int) - asserts string length >= minLength - HasMaxLength(int) - asserts string length <= maxLength - HasLengthBetween(int, int) - asserts min <= string length <= max Closes #4868 --- .../Conditions/StringAssertions.cs | 126 ++++++++++++++++++ .../Extensions/AssertionExtensions.cs | 41 ++++++ 2 files changed, 167 insertions(+) diff --git a/TUnit.Assertions/Conditions/StringAssertions.cs b/TUnit.Assertions/Conditions/StringAssertions.cs index 8879b37f08..875b89fac7 100644 --- a/TUnit.Assertions/Conditions/StringAssertions.cs +++ b/TUnit.Assertions/Conditions/StringAssertions.cs @@ -717,3 +717,129 @@ protected override string GetExpectation() return "to not match pattern"; } } + +/// +/// Asserts that a string has a length greater than or equal to the specified minimum. +/// +public class StringMinLengthAssertion : Assertion +{ + private readonly int _minLength; + + public StringMinLengthAssertion( + AssertionContext context, + int minLength) + : base(context) + { + _minLength = minLength; + } + + protected override Task CheckAsync(EvaluationMetadata metadata) + { + var value = metadata.Value; + var exception = metadata.Exception; + + if (exception != null) + { + return Task.FromResult(AssertionResult.Failed($"threw {exception.GetType().Name}")); + } + + if (value == null) + { + return Task.FromResult(AssertionResult.Failed("value was null")); + } + + if (value.Length >= _minLength) + { + return AssertionResult._passedTask; + } + + return Task.FromResult(AssertionResult.Failed($"found length {value.Length}")); + } + + protected override string GetExpectation() => $"to have a minimum length of {_minLength}"; +} + +/// +/// Asserts that a string has a length less than or equal to the specified maximum. +/// +public class StringMaxLengthAssertion : Assertion +{ + private readonly int _maxLength; + + public StringMaxLengthAssertion( + AssertionContext context, + int maxLength) + : base(context) + { + _maxLength = maxLength; + } + + protected override Task CheckAsync(EvaluationMetadata metadata) + { + var value = metadata.Value; + var exception = metadata.Exception; + + if (exception != null) + { + return Task.FromResult(AssertionResult.Failed($"threw {exception.GetType().Name}")); + } + + if (value == null) + { + return Task.FromResult(AssertionResult.Failed("value was null")); + } + + if (value.Length <= _maxLength) + { + return AssertionResult._passedTask; + } + + return Task.FromResult(AssertionResult.Failed($"found length {value.Length}")); + } + + protected override string GetExpectation() => $"to have a maximum length of {_maxLength}"; +} + +/// +/// Asserts that a string has a length between the specified minimum and maximum (inclusive). +/// +public class StringLengthBetweenAssertion : Assertion +{ + private readonly int _minLength; + private readonly int _maxLength; + + public StringLengthBetweenAssertion( + AssertionContext context, + int minLength, + int maxLength) + : base(context) + { + _minLength = minLength; + _maxLength = maxLength; + } + + protected override Task CheckAsync(EvaluationMetadata metadata) + { + var value = metadata.Value; + var exception = metadata.Exception; + + if (exception != null) + { + return Task.FromResult(AssertionResult.Failed($"threw {exception.GetType().Name}")); + } + + if (value == null) + { + return Task.FromResult(AssertionResult.Failed("value was null")); + } + + if (value.Length >= _minLength && value.Length <= _maxLength) + { + return AssertionResult._passedTask; + } + + return Task.FromResult(AssertionResult.Failed($"found length {value.Length}")); + } + + protected override string GetExpectation() => $"to have length between {_minLength} and {_maxLength}"; +} diff --git a/TUnit.Assertions/Extensions/AssertionExtensions.cs b/TUnit.Assertions/Extensions/AssertionExtensions.cs index 97b713d664..3cbcc4c612 100644 --- a/TUnit.Assertions/Extensions/AssertionExtensions.cs +++ b/TUnit.Assertions/Extensions/AssertionExtensions.cs @@ -723,6 +723,47 @@ public static StringLengthAssertion HasLength( return new StringLengthAssertion(source.Context, expectedLength); } + /// + /// Asserts that the string has a length greater than or equal to the specified minimum. + /// Example: await Assert.That(str).HasMinLength(3); + /// + public static StringMinLengthAssertion HasMinLength( + this IAssertionSource source, + int minLength, + [CallerArgumentExpression(nameof(minLength))] string? expression = null) + { + source.Context.ExpressionBuilder.Append($".HasMinLength({expression})"); + return new StringMinLengthAssertion(source.Context, minLength); + } + + /// + /// Asserts that the string has a length less than or equal to the specified maximum. + /// Example: await Assert.That(str).HasMaxLength(10); + /// + public static StringMaxLengthAssertion HasMaxLength( + this IAssertionSource source, + int maxLength, + [CallerArgumentExpression(nameof(maxLength))] string? expression = null) + { + source.Context.ExpressionBuilder.Append($".HasMaxLength({expression})"); + return new StringMaxLengthAssertion(source.Context, maxLength); + } + + /// + /// Asserts that the string has a length between the specified minimum and maximum (inclusive). + /// Example: await Assert.That(str).HasLengthBetween(3, 10); + /// + public static StringLengthBetweenAssertion HasLengthBetween( + this IAssertionSource source, + int minLength, + int maxLength, + [CallerArgumentExpression(nameof(minLength))] string? minExpression = null, + [CallerArgumentExpression(nameof(maxLength))] string? maxExpression = null) + { + source.Context.ExpressionBuilder.Append($".HasLengthBetween({minExpression}, {maxExpression})"); + return new StringLengthBetweenAssertion(source.Context, minLength, maxLength); + } + /// /// Asserts that the value is structurally equivalent to the expected value. /// Performs deep comparison of properties and fields. From 019eb55aa3f0fea79db5c950712032c6b1332527 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 23 Mar 2026 13:41:14 +0000 Subject: [PATCH 2/4] chore: update source generator snapshot files after rebase --- ...ExceptionAssertions.DotNet8_0.verified.txt | 60 ++++++++++++------- ...ExceptionAssertions.DotNet9_0.verified.txt | 60 ++++++++++++------- ...tesExceptionAssertions.Net4_7.verified.txt | 60 ++++++++++++------- ...tionResultOfTMethod.DotNet8_0.verified.txt | 6 +- ...tionResultOfTMethod.DotNet9_0.verified.txt | 6 +- ...sertionResultOfTMethod.Net4_7.verified.txt | 6 +- ...tionResultOfTMethod.DotNet8_0.verified.txt | 6 +- ...tionResultOfTMethod.DotNet9_0.verified.txt | 6 +- ...sertionResultOfTMethod.Net4_7.verified.txt | 6 +- 9 files changed, 144 insertions(+), 72 deletions(-) diff --git a/TUnit.Assertions.SourceGenerator.Tests/ExceptionAssertionGeneratorTests.GeneratesExceptionAssertions.DotNet8_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/ExceptionAssertionGeneratorTests.GeneratesExceptionAssertions.DotNet8_0.verified.txt index 776124a663..4c2caae9f8 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/ExceptionAssertionGeneratorTests.GeneratesExceptionAssertions.DotNet8_0.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/ExceptionAssertionGeneratorTests.GeneratesExceptionAssertions.DotNet8_0.verified.txt @@ -406,93 +406,113 @@ public static partial class ExceptionAssertionExtensions /// /// Generated extension method for HasInnerException /// - public static Exception_HasInnerException_Assertion HasInnerException(this IAssertionSource source) + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] + public static Exception_HasInnerException_Assertion HasInnerException(this IAssertionSource source) + where TActual : System.Exception { source.Context.ExpressionBuilder.Append(".HasInnerException()"); - return new Exception_HasInnerException_Assertion(source.Context); + return new Exception_HasInnerException_Assertion(source.Context.Map(static x => (System.Exception?)x)); } /// /// Generated extension method for HasNoInnerException /// - public static Exception_HasNoInnerException_Assertion HasNoInnerException(this IAssertionSource source) + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] + public static Exception_HasNoInnerException_Assertion HasNoInnerException(this IAssertionSource source) + where TActual : System.Exception { source.Context.ExpressionBuilder.Append(".HasNoInnerException()"); - return new Exception_HasNoInnerException_Assertion(source.Context); + return new Exception_HasNoInnerException_Assertion(source.Context.Map(static x => (System.Exception?)x)); } /// /// Generated extension method for HasStackTrace /// - public static Exception_HasStackTrace_Assertion HasStackTrace(this IAssertionSource source) + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] + public static Exception_HasStackTrace_Assertion HasStackTrace(this IAssertionSource source) + where TActual : System.Exception { source.Context.ExpressionBuilder.Append(".HasStackTrace()"); - return new Exception_HasStackTrace_Assertion(source.Context); + return new Exception_HasStackTrace_Assertion(source.Context.Map(static x => (System.Exception?)x)); } /// /// Generated extension method for HasNoData /// - public static Exception_HasNoData_Assertion HasNoData(this IAssertionSource source) + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] + public static Exception_HasNoData_Assertion HasNoData(this IAssertionSource source) + where TActual : System.Exception { source.Context.ExpressionBuilder.Append(".HasNoData()"); - return new Exception_HasNoData_Assertion(source.Context); + return new Exception_HasNoData_Assertion(source.Context.Map(static x => (System.Exception?)x)); } /// /// Generated extension method for HasHelpLink /// - public static Exception_HasHelpLink_Assertion HasHelpLink(this IAssertionSource source) + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] + public static Exception_HasHelpLink_Assertion HasHelpLink(this IAssertionSource source) + where TActual : System.Exception { source.Context.ExpressionBuilder.Append(".HasHelpLink()"); - return new Exception_HasHelpLink_Assertion(source.Context); + return new Exception_HasHelpLink_Assertion(source.Context.Map(static x => (System.Exception?)x)); } /// /// Generated extension method for HasNoHelpLink /// - public static Exception_HasNoHelpLink_Assertion HasNoHelpLink(this IAssertionSource source) + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] + public static Exception_HasNoHelpLink_Assertion HasNoHelpLink(this IAssertionSource source) + where TActual : System.Exception { source.Context.ExpressionBuilder.Append(".HasNoHelpLink()"); - return new Exception_HasNoHelpLink_Assertion(source.Context); + return new Exception_HasNoHelpLink_Assertion(source.Context.Map(static x => (System.Exception?)x)); } /// /// Generated extension method for HasSource /// - public static Exception_HasSource_Assertion HasSource(this IAssertionSource source) + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] + public static Exception_HasSource_Assertion HasSource(this IAssertionSource source) + where TActual : System.Exception { source.Context.ExpressionBuilder.Append(".HasSource()"); - return new Exception_HasSource_Assertion(source.Context); + return new Exception_HasSource_Assertion(source.Context.Map(static x => (System.Exception?)x)); } /// /// Generated extension method for HasNoSource /// - public static Exception_HasNoSource_Assertion HasNoSource(this IAssertionSource source) + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] + public static Exception_HasNoSource_Assertion HasNoSource(this IAssertionSource source) + where TActual : System.Exception { source.Context.ExpressionBuilder.Append(".HasNoSource()"); - return new Exception_HasNoSource_Assertion(source.Context); + return new Exception_HasNoSource_Assertion(source.Context.Map(static x => (System.Exception?)x)); } /// /// Generated extension method for HasTargetSite /// + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Exception.TargetSite uses reflection which may be trimmed in AOT scenarios")] - public static Exception_HasTargetSite_Assertion HasTargetSite(this IAssertionSource source) + public static Exception_HasTargetSite_Assertion HasTargetSite(this IAssertionSource source) + where TActual : System.Exception { source.Context.ExpressionBuilder.Append(".HasTargetSite()"); - return new Exception_HasTargetSite_Assertion(source.Context); + return new Exception_HasTargetSite_Assertion(source.Context.Map(static x => (System.Exception?)x)); } /// /// Generated extension method for HasNoTargetSite /// + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Exception.TargetSite uses reflection which may be trimmed in AOT scenarios")] - public static Exception_HasNoTargetSite_Assertion HasNoTargetSite(this IAssertionSource source) + public static Exception_HasNoTargetSite_Assertion HasNoTargetSite(this IAssertionSource source) + where TActual : System.Exception { source.Context.ExpressionBuilder.Append(".HasNoTargetSite()"); - return new Exception_HasNoTargetSite_Assertion(source.Context); + return new Exception_HasNoTargetSite_Assertion(source.Context.Map(static x => (System.Exception?)x)); } } diff --git a/TUnit.Assertions.SourceGenerator.Tests/ExceptionAssertionGeneratorTests.GeneratesExceptionAssertions.DotNet9_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/ExceptionAssertionGeneratorTests.GeneratesExceptionAssertions.DotNet9_0.verified.txt index 776124a663..4c2caae9f8 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/ExceptionAssertionGeneratorTests.GeneratesExceptionAssertions.DotNet9_0.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/ExceptionAssertionGeneratorTests.GeneratesExceptionAssertions.DotNet9_0.verified.txt @@ -406,93 +406,113 @@ public static partial class ExceptionAssertionExtensions /// /// Generated extension method for HasInnerException /// - public static Exception_HasInnerException_Assertion HasInnerException(this IAssertionSource source) + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] + public static Exception_HasInnerException_Assertion HasInnerException(this IAssertionSource source) + where TActual : System.Exception { source.Context.ExpressionBuilder.Append(".HasInnerException()"); - return new Exception_HasInnerException_Assertion(source.Context); + return new Exception_HasInnerException_Assertion(source.Context.Map(static x => (System.Exception?)x)); } /// /// Generated extension method for HasNoInnerException /// - public static Exception_HasNoInnerException_Assertion HasNoInnerException(this IAssertionSource source) + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] + public static Exception_HasNoInnerException_Assertion HasNoInnerException(this IAssertionSource source) + where TActual : System.Exception { source.Context.ExpressionBuilder.Append(".HasNoInnerException()"); - return new Exception_HasNoInnerException_Assertion(source.Context); + return new Exception_HasNoInnerException_Assertion(source.Context.Map(static x => (System.Exception?)x)); } /// /// Generated extension method for HasStackTrace /// - public static Exception_HasStackTrace_Assertion HasStackTrace(this IAssertionSource source) + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] + public static Exception_HasStackTrace_Assertion HasStackTrace(this IAssertionSource source) + where TActual : System.Exception { source.Context.ExpressionBuilder.Append(".HasStackTrace()"); - return new Exception_HasStackTrace_Assertion(source.Context); + return new Exception_HasStackTrace_Assertion(source.Context.Map(static x => (System.Exception?)x)); } /// /// Generated extension method for HasNoData /// - public static Exception_HasNoData_Assertion HasNoData(this IAssertionSource source) + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] + public static Exception_HasNoData_Assertion HasNoData(this IAssertionSource source) + where TActual : System.Exception { source.Context.ExpressionBuilder.Append(".HasNoData()"); - return new Exception_HasNoData_Assertion(source.Context); + return new Exception_HasNoData_Assertion(source.Context.Map(static x => (System.Exception?)x)); } /// /// Generated extension method for HasHelpLink /// - public static Exception_HasHelpLink_Assertion HasHelpLink(this IAssertionSource source) + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] + public static Exception_HasHelpLink_Assertion HasHelpLink(this IAssertionSource source) + where TActual : System.Exception { source.Context.ExpressionBuilder.Append(".HasHelpLink()"); - return new Exception_HasHelpLink_Assertion(source.Context); + return new Exception_HasHelpLink_Assertion(source.Context.Map(static x => (System.Exception?)x)); } /// /// Generated extension method for HasNoHelpLink /// - public static Exception_HasNoHelpLink_Assertion HasNoHelpLink(this IAssertionSource source) + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] + public static Exception_HasNoHelpLink_Assertion HasNoHelpLink(this IAssertionSource source) + where TActual : System.Exception { source.Context.ExpressionBuilder.Append(".HasNoHelpLink()"); - return new Exception_HasNoHelpLink_Assertion(source.Context); + return new Exception_HasNoHelpLink_Assertion(source.Context.Map(static x => (System.Exception?)x)); } /// /// Generated extension method for HasSource /// - public static Exception_HasSource_Assertion HasSource(this IAssertionSource source) + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] + public static Exception_HasSource_Assertion HasSource(this IAssertionSource source) + where TActual : System.Exception { source.Context.ExpressionBuilder.Append(".HasSource()"); - return new Exception_HasSource_Assertion(source.Context); + return new Exception_HasSource_Assertion(source.Context.Map(static x => (System.Exception?)x)); } /// /// Generated extension method for HasNoSource /// - public static Exception_HasNoSource_Assertion HasNoSource(this IAssertionSource source) + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] + public static Exception_HasNoSource_Assertion HasNoSource(this IAssertionSource source) + where TActual : System.Exception { source.Context.ExpressionBuilder.Append(".HasNoSource()"); - return new Exception_HasNoSource_Assertion(source.Context); + return new Exception_HasNoSource_Assertion(source.Context.Map(static x => (System.Exception?)x)); } /// /// Generated extension method for HasTargetSite /// + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Exception.TargetSite uses reflection which may be trimmed in AOT scenarios")] - public static Exception_HasTargetSite_Assertion HasTargetSite(this IAssertionSource source) + public static Exception_HasTargetSite_Assertion HasTargetSite(this IAssertionSource source) + where TActual : System.Exception { source.Context.ExpressionBuilder.Append(".HasTargetSite()"); - return new Exception_HasTargetSite_Assertion(source.Context); + return new Exception_HasTargetSite_Assertion(source.Context.Map(static x => (System.Exception?)x)); } /// /// Generated extension method for HasNoTargetSite /// + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Exception.TargetSite uses reflection which may be trimmed in AOT scenarios")] - public static Exception_HasNoTargetSite_Assertion HasNoTargetSite(this IAssertionSource source) + public static Exception_HasNoTargetSite_Assertion HasNoTargetSite(this IAssertionSource source) + where TActual : System.Exception { source.Context.ExpressionBuilder.Append(".HasNoTargetSite()"); - return new Exception_HasNoTargetSite_Assertion(source.Context); + return new Exception_HasNoTargetSite_Assertion(source.Context.Map(static x => (System.Exception?)x)); } } diff --git a/TUnit.Assertions.SourceGenerator.Tests/ExceptionAssertionGeneratorTests.GeneratesExceptionAssertions.Net4_7.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/ExceptionAssertionGeneratorTests.GeneratesExceptionAssertions.Net4_7.verified.txt index 1300707a92..a4f7f65aae 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/ExceptionAssertionGeneratorTests.GeneratesExceptionAssertions.Net4_7.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/ExceptionAssertionGeneratorTests.GeneratesExceptionAssertions.Net4_7.verified.txt @@ -404,91 +404,111 @@ public static partial class ExceptionAssertionExtensions /// /// Generated extension method for HasInnerException /// - public static Exception_HasInnerException_Assertion HasInnerException(this IAssertionSource source) + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] + public static Exception_HasInnerException_Assertion HasInnerException(this IAssertionSource source) + where TActual : System.Exception { source.Context.ExpressionBuilder.Append(".HasInnerException()"); - return new Exception_HasInnerException_Assertion(source.Context); + return new Exception_HasInnerException_Assertion(source.Context.Map(static x => (System.Exception?)x)); } /// /// Generated extension method for HasNoInnerException /// - public static Exception_HasNoInnerException_Assertion HasNoInnerException(this IAssertionSource source) + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] + public static Exception_HasNoInnerException_Assertion HasNoInnerException(this IAssertionSource source) + where TActual : System.Exception { source.Context.ExpressionBuilder.Append(".HasNoInnerException()"); - return new Exception_HasNoInnerException_Assertion(source.Context); + return new Exception_HasNoInnerException_Assertion(source.Context.Map(static x => (System.Exception?)x)); } /// /// Generated extension method for HasStackTrace /// - public static Exception_HasStackTrace_Assertion HasStackTrace(this IAssertionSource source) + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] + public static Exception_HasStackTrace_Assertion HasStackTrace(this IAssertionSource source) + where TActual : System.Exception { source.Context.ExpressionBuilder.Append(".HasStackTrace()"); - return new Exception_HasStackTrace_Assertion(source.Context); + return new Exception_HasStackTrace_Assertion(source.Context.Map(static x => (System.Exception?)x)); } /// /// Generated extension method for HasNoData /// - public static Exception_HasNoData_Assertion HasNoData(this IAssertionSource source) + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] + public static Exception_HasNoData_Assertion HasNoData(this IAssertionSource source) + where TActual : System.Exception { source.Context.ExpressionBuilder.Append(".HasNoData()"); - return new Exception_HasNoData_Assertion(source.Context); + return new Exception_HasNoData_Assertion(source.Context.Map(static x => (System.Exception?)x)); } /// /// Generated extension method for HasHelpLink /// - public static Exception_HasHelpLink_Assertion HasHelpLink(this IAssertionSource source) + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] + public static Exception_HasHelpLink_Assertion HasHelpLink(this IAssertionSource source) + where TActual : System.Exception { source.Context.ExpressionBuilder.Append(".HasHelpLink()"); - return new Exception_HasHelpLink_Assertion(source.Context); + return new Exception_HasHelpLink_Assertion(source.Context.Map(static x => (System.Exception?)x)); } /// /// Generated extension method for HasNoHelpLink /// - public static Exception_HasNoHelpLink_Assertion HasNoHelpLink(this IAssertionSource source) + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] + public static Exception_HasNoHelpLink_Assertion HasNoHelpLink(this IAssertionSource source) + where TActual : System.Exception { source.Context.ExpressionBuilder.Append(".HasNoHelpLink()"); - return new Exception_HasNoHelpLink_Assertion(source.Context); + return new Exception_HasNoHelpLink_Assertion(source.Context.Map(static x => (System.Exception?)x)); } /// /// Generated extension method for HasSource /// - public static Exception_HasSource_Assertion HasSource(this IAssertionSource source) + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] + public static Exception_HasSource_Assertion HasSource(this IAssertionSource source) + where TActual : System.Exception { source.Context.ExpressionBuilder.Append(".HasSource()"); - return new Exception_HasSource_Assertion(source.Context); + return new Exception_HasSource_Assertion(source.Context.Map(static x => (System.Exception?)x)); } /// /// Generated extension method for HasNoSource /// - public static Exception_HasNoSource_Assertion HasNoSource(this IAssertionSource source) + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] + public static Exception_HasNoSource_Assertion HasNoSource(this IAssertionSource source) + where TActual : System.Exception { source.Context.ExpressionBuilder.Append(".HasNoSource()"); - return new Exception_HasNoSource_Assertion(source.Context); + return new Exception_HasNoSource_Assertion(source.Context.Map(static x => (System.Exception?)x)); } /// /// Generated extension method for HasTargetSite /// - public static Exception_HasTargetSite_Assertion HasTargetSite(this IAssertionSource source) + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] + public static Exception_HasTargetSite_Assertion HasTargetSite(this IAssertionSource source) + where TActual : System.Exception { source.Context.ExpressionBuilder.Append(".HasTargetSite()"); - return new Exception_HasTargetSite_Assertion(source.Context); + return new Exception_HasTargetSite_Assertion(source.Context.Map(static x => (System.Exception?)x)); } /// /// Generated extension method for HasNoTargetSite /// - public static Exception_HasNoTargetSite_Assertion HasNoTargetSite(this IAssertionSource source) + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] + public static Exception_HasNoTargetSite_Assertion HasNoTargetSite(this IAssertionSource source) + where TActual : System.Exception { source.Context.ExpressionBuilder.Append(".HasNoTargetSite()"); - return new Exception_HasNoTargetSite_Assertion(source.Context); + return new Exception_HasNoTargetSite_Assertion(source.Context.Map(static x => (System.Exception?)x)); } } diff --git a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.AssertionResultOfTMethod.DotNet8_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.AssertionResultOfTMethod.DotNet8_0.verified.txt index 56eb54174c..62c15bf931 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.AssertionResultOfTMethod.DotNet8_0.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.AssertionResultOfTMethod.DotNet8_0.verified.txt @@ -70,10 +70,12 @@ public static partial class AssertionResultOfTMethodExtensions /// /// Generated extension method for ContainsMatch /// - public static IEnumerableString_ContainsMatch_String_Assertion ContainsMatch(this IAssertionSource> source, string needle, [CallerArgumentExpression(nameof(needle))] string? needleExpression = null) + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] + public static IEnumerableString_ContainsMatch_String_Assertion ContainsMatch(this IAssertionSource source, string needle, [CallerArgumentExpression(nameof(needle))] string? needleExpression = null) + where TActual : System.Collections.Generic.IEnumerable { source.Context.ExpressionBuilder.Append($".ContainsMatch({needleExpression})"); - return new IEnumerableString_ContainsMatch_String_Assertion(source.Context, needle); + return new IEnumerableString_ContainsMatch_String_Assertion(source.Context.Map>(static x => (System.Collections.Generic.IEnumerable?)x), needle); } } diff --git a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.AssertionResultOfTMethod.DotNet9_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.AssertionResultOfTMethod.DotNet9_0.verified.txt index 56eb54174c..62c15bf931 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.AssertionResultOfTMethod.DotNet9_0.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.AssertionResultOfTMethod.DotNet9_0.verified.txt @@ -70,10 +70,12 @@ public static partial class AssertionResultOfTMethodExtensions /// /// Generated extension method for ContainsMatch /// - public static IEnumerableString_ContainsMatch_String_Assertion ContainsMatch(this IAssertionSource> source, string needle, [CallerArgumentExpression(nameof(needle))] string? needleExpression = null) + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] + public static IEnumerableString_ContainsMatch_String_Assertion ContainsMatch(this IAssertionSource source, string needle, [CallerArgumentExpression(nameof(needle))] string? needleExpression = null) + where TActual : System.Collections.Generic.IEnumerable { source.Context.ExpressionBuilder.Append($".ContainsMatch({needleExpression})"); - return new IEnumerableString_ContainsMatch_String_Assertion(source.Context, needle); + return new IEnumerableString_ContainsMatch_String_Assertion(source.Context.Map>(static x => (System.Collections.Generic.IEnumerable?)x), needle); } } diff --git a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.AssertionResultOfTMethod.Net4_7.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.AssertionResultOfTMethod.Net4_7.verified.txt index 56eb54174c..62c15bf931 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.AssertionResultOfTMethod.Net4_7.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.AssertionResultOfTMethod.Net4_7.verified.txt @@ -70,10 +70,12 @@ public static partial class AssertionResultOfTMethodExtensions /// /// Generated extension method for ContainsMatch /// - public static IEnumerableString_ContainsMatch_String_Assertion ContainsMatch(this IAssertionSource> source, string needle, [CallerArgumentExpression(nameof(needle))] string? needleExpression = null) + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] + public static IEnumerableString_ContainsMatch_String_Assertion ContainsMatch(this IAssertionSource source, string needle, [CallerArgumentExpression(nameof(needle))] string? needleExpression = null) + where TActual : System.Collections.Generic.IEnumerable { source.Context.ExpressionBuilder.Append($".ContainsMatch({needleExpression})"); - return new IEnumerableString_ContainsMatch_String_Assertion(source.Context, needle); + return new IEnumerableString_ContainsMatch_String_Assertion(source.Context.Map>(static x => (System.Collections.Generic.IEnumerable?)x), needle); } } diff --git a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.AsyncAssertionResultOfTMethod.DotNet8_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.AsyncAssertionResultOfTMethod.DotNet8_0.verified.txt index 25500147f3..a6a2aa30eb 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.AsyncAssertionResultOfTMethod.DotNet8_0.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.AsyncAssertionResultOfTMethod.DotNet8_0.verified.txt @@ -70,10 +70,12 @@ public static partial class AsyncAssertionResultOfTMethodExtensions /// /// Generated extension method for ContainsMatchAsync /// - public static IEnumerableString_ContainsMatchAsync_String_Assertion ContainsMatchAsync(this IAssertionSource> source, string needle, [CallerArgumentExpression(nameof(needle))] string? needleExpression = null) + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] + public static IEnumerableString_ContainsMatchAsync_String_Assertion ContainsMatchAsync(this IAssertionSource source, string needle, [CallerArgumentExpression(nameof(needle))] string? needleExpression = null) + where TActual : System.Collections.Generic.IEnumerable { source.Context.ExpressionBuilder.Append($".ContainsMatchAsync({needleExpression})"); - return new IEnumerableString_ContainsMatchAsync_String_Assertion(source.Context, needle); + return new IEnumerableString_ContainsMatchAsync_String_Assertion(source.Context.Map>(static x => (System.Collections.Generic.IEnumerable?)x), needle); } } diff --git a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.AsyncAssertionResultOfTMethod.DotNet9_0.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.AsyncAssertionResultOfTMethod.DotNet9_0.verified.txt index 25500147f3..a6a2aa30eb 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.AsyncAssertionResultOfTMethod.DotNet9_0.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.AsyncAssertionResultOfTMethod.DotNet9_0.verified.txt @@ -70,10 +70,12 @@ public static partial class AsyncAssertionResultOfTMethodExtensions /// /// Generated extension method for ContainsMatchAsync /// - public static IEnumerableString_ContainsMatchAsync_String_Assertion ContainsMatchAsync(this IAssertionSource> source, string needle, [CallerArgumentExpression(nameof(needle))] string? needleExpression = null) + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] + public static IEnumerableString_ContainsMatchAsync_String_Assertion ContainsMatchAsync(this IAssertionSource source, string needle, [CallerArgumentExpression(nameof(needle))] string? needleExpression = null) + where TActual : System.Collections.Generic.IEnumerable { source.Context.ExpressionBuilder.Append($".ContainsMatchAsync({needleExpression})"); - return new IEnumerableString_ContainsMatchAsync_String_Assertion(source.Context, needle); + return new IEnumerableString_ContainsMatchAsync_String_Assertion(source.Context.Map>(static x => (System.Collections.Generic.IEnumerable?)x), needle); } } diff --git a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.AsyncAssertionResultOfTMethod.Net4_7.verified.txt b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.AsyncAssertionResultOfTMethod.Net4_7.verified.txt index 25500147f3..a6a2aa30eb 100644 --- a/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.AsyncAssertionResultOfTMethod.Net4_7.verified.txt +++ b/TUnit.Assertions.SourceGenerator.Tests/MethodAssertionGeneratorTests.AsyncAssertionResultOfTMethod.Net4_7.verified.txt @@ -70,10 +70,12 @@ public static partial class AsyncAssertionResultOfTMethodExtensions /// /// Generated extension method for ContainsMatchAsync /// - public static IEnumerableString_ContainsMatchAsync_String_Assertion ContainsMatchAsync(this IAssertionSource> source, string needle, [CallerArgumentExpression(nameof(needle))] string? needleExpression = null) + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Generic type parameter is only used for property access, not instantiation")] + public static IEnumerableString_ContainsMatchAsync_String_Assertion ContainsMatchAsync(this IAssertionSource source, string needle, [CallerArgumentExpression(nameof(needle))] string? needleExpression = null) + where TActual : System.Collections.Generic.IEnumerable { source.Context.ExpressionBuilder.Append($".ContainsMatchAsync({needleExpression})"); - return new IEnumerableString_ContainsMatchAsync_String_Assertion(source.Context, needle); + return new IEnumerableString_ContainsMatchAsync_String_Assertion(source.Context.Map>(static x => (System.Collections.Generic.IEnumerable?)x), needle); } } From 105d7008390b642a2967117e67565ddf26f285e7 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 23 Mar 2026 13:41:24 +0000 Subject: [PATCH 3/4] refactor: use [GenerateAssertion] for string length assertions and add bounds guard Convert HasMinLength, HasMaxLength, HasLengthBetween from manual assertion classes to [GenerateAssertion] source-generated assertions, reducing ~170 lines of boilerplate. Add ArgumentOutOfRangeException guard when minLength > maxLength in HasLengthBetween. --- .../Conditions/StringAssertions.cs | 126 ------------------ .../StringLengthRangeAssertionExtensions.cs | 45 +++++++ .../Extensions/AssertionExtensions.cs | 41 ------ ...Has_No_API_Changes.DotNet10_0.verified.txt | 33 +++++ ..._Has_No_API_Changes.DotNet8_0.verified.txt | 33 +++++ ..._Has_No_API_Changes.DotNet9_0.verified.txt | 33 +++++ ...ary_Has_No_API_Changes.Net4_7.verified.txt | 33 +++++ 7 files changed, 177 insertions(+), 167 deletions(-) create mode 100644 TUnit.Assertions/Conditions/StringLengthRangeAssertionExtensions.cs diff --git a/TUnit.Assertions/Conditions/StringAssertions.cs b/TUnit.Assertions/Conditions/StringAssertions.cs index 875b89fac7..8879b37f08 100644 --- a/TUnit.Assertions/Conditions/StringAssertions.cs +++ b/TUnit.Assertions/Conditions/StringAssertions.cs @@ -717,129 +717,3 @@ protected override string GetExpectation() return "to not match pattern"; } } - -/// -/// Asserts that a string has a length greater than or equal to the specified minimum. -/// -public class StringMinLengthAssertion : Assertion -{ - private readonly int _minLength; - - public StringMinLengthAssertion( - AssertionContext context, - int minLength) - : base(context) - { - _minLength = minLength; - } - - protected override Task CheckAsync(EvaluationMetadata metadata) - { - var value = metadata.Value; - var exception = metadata.Exception; - - if (exception != null) - { - return Task.FromResult(AssertionResult.Failed($"threw {exception.GetType().Name}")); - } - - if (value == null) - { - return Task.FromResult(AssertionResult.Failed("value was null")); - } - - if (value.Length >= _minLength) - { - return AssertionResult._passedTask; - } - - return Task.FromResult(AssertionResult.Failed($"found length {value.Length}")); - } - - protected override string GetExpectation() => $"to have a minimum length of {_minLength}"; -} - -/// -/// Asserts that a string has a length less than or equal to the specified maximum. -/// -public class StringMaxLengthAssertion : Assertion -{ - private readonly int _maxLength; - - public StringMaxLengthAssertion( - AssertionContext context, - int maxLength) - : base(context) - { - _maxLength = maxLength; - } - - protected override Task CheckAsync(EvaluationMetadata metadata) - { - var value = metadata.Value; - var exception = metadata.Exception; - - if (exception != null) - { - return Task.FromResult(AssertionResult.Failed($"threw {exception.GetType().Name}")); - } - - if (value == null) - { - return Task.FromResult(AssertionResult.Failed("value was null")); - } - - if (value.Length <= _maxLength) - { - return AssertionResult._passedTask; - } - - return Task.FromResult(AssertionResult.Failed($"found length {value.Length}")); - } - - protected override string GetExpectation() => $"to have a maximum length of {_maxLength}"; -} - -/// -/// Asserts that a string has a length between the specified minimum and maximum (inclusive). -/// -public class StringLengthBetweenAssertion : Assertion -{ - private readonly int _minLength; - private readonly int _maxLength; - - public StringLengthBetweenAssertion( - AssertionContext context, - int minLength, - int maxLength) - : base(context) - { - _minLength = minLength; - _maxLength = maxLength; - } - - protected override Task CheckAsync(EvaluationMetadata metadata) - { - var value = metadata.Value; - var exception = metadata.Exception; - - if (exception != null) - { - return Task.FromResult(AssertionResult.Failed($"threw {exception.GetType().Name}")); - } - - if (value == null) - { - return Task.FromResult(AssertionResult.Failed("value was null")); - } - - if (value.Length >= _minLength && value.Length <= _maxLength) - { - return AssertionResult._passedTask; - } - - return Task.FromResult(AssertionResult.Failed($"found length {value.Length}")); - } - - protected override string GetExpectation() => $"to have length between {_minLength} and {_maxLength}"; -} diff --git a/TUnit.Assertions/Conditions/StringLengthRangeAssertionExtensions.cs b/TUnit.Assertions/Conditions/StringLengthRangeAssertionExtensions.cs new file mode 100644 index 0000000000..bf27c18f81 --- /dev/null +++ b/TUnit.Assertions/Conditions/StringLengthRangeAssertionExtensions.cs @@ -0,0 +1,45 @@ +using System; +using System.ComponentModel; +using TUnit.Assertions.Attributes; +using TUnit.Assertions.Core; + +namespace TUnit.Assertions.Conditions; + +/// +/// Source-generated string length range assertions using [GenerateAssertion] attributes. +/// +public static partial class StringLengthRangeAssertionExtensions +{ + [EditorBrowsable(EditorBrowsableState.Never)] + [GenerateAssertion(ExpectationMessage = "to have a minimum length of {minLength}")] + public static AssertionResult HasMinLength(this string value, int minLength) + { + return value.Length >= minLength + ? AssertionResult.Passed + : AssertionResult.Failed($"found length {value.Length}"); + } + + [EditorBrowsable(EditorBrowsableState.Never)] + [GenerateAssertion(ExpectationMessage = "to have a maximum length of {maxLength}")] + public static AssertionResult HasMaxLength(this string value, int maxLength) + { + return value.Length <= maxLength + ? AssertionResult.Passed + : AssertionResult.Failed($"found length {value.Length}"); + } + + [EditorBrowsable(EditorBrowsableState.Never)] + [GenerateAssertion(ExpectationMessage = "to have length between {minLength} and {maxLength}")] + public static AssertionResult HasLengthBetween(this string value, int minLength, int maxLength) + { + if (minLength > maxLength) + { + throw new ArgumentOutOfRangeException(nameof(minLength), + $"minLength ({minLength}) must be less than or equal to maxLength ({maxLength})."); + } + + return value.Length >= minLength && value.Length <= maxLength + ? AssertionResult.Passed + : AssertionResult.Failed($"found length {value.Length}"); + } +} diff --git a/TUnit.Assertions/Extensions/AssertionExtensions.cs b/TUnit.Assertions/Extensions/AssertionExtensions.cs index 3cbcc4c612..97b713d664 100644 --- a/TUnit.Assertions/Extensions/AssertionExtensions.cs +++ b/TUnit.Assertions/Extensions/AssertionExtensions.cs @@ -723,47 +723,6 @@ public static StringLengthAssertion HasLength( return new StringLengthAssertion(source.Context, expectedLength); } - /// - /// Asserts that the string has a length greater than or equal to the specified minimum. - /// Example: await Assert.That(str).HasMinLength(3); - /// - public static StringMinLengthAssertion HasMinLength( - this IAssertionSource source, - int minLength, - [CallerArgumentExpression(nameof(minLength))] string? expression = null) - { - source.Context.ExpressionBuilder.Append($".HasMinLength({expression})"); - return new StringMinLengthAssertion(source.Context, minLength); - } - - /// - /// Asserts that the string has a length less than or equal to the specified maximum. - /// Example: await Assert.That(str).HasMaxLength(10); - /// - public static StringMaxLengthAssertion HasMaxLength( - this IAssertionSource source, - int maxLength, - [CallerArgumentExpression(nameof(maxLength))] string? expression = null) - { - source.Context.ExpressionBuilder.Append($".HasMaxLength({expression})"); - return new StringMaxLengthAssertion(source.Context, maxLength); - } - - /// - /// Asserts that the string has a length between the specified minimum and maximum (inclusive). - /// Example: await Assert.That(str).HasLengthBetween(3, 10); - /// - public static StringLengthBetweenAssertion HasLengthBetween( - this IAssertionSource source, - int minLength, - int maxLength, - [CallerArgumentExpression(nameof(minLength))] string? minExpression = null, - [CallerArgumentExpression(nameof(maxLength))] string? maxExpression = null) - { - source.Context.ExpressionBuilder.Append($".HasLengthBetween({minExpression}, {maxExpression})"); - return new StringLengthBetweenAssertion(source.Context, minLength, maxLength); - } - /// /// Asserts that the value is structurally equivalent to the expected value. /// Performs deep comparison of properties and fields. 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 3c92472c75..b897d0c5d6 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 @@ -2035,6 +2035,15 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } + public static class StringLengthRangeAssertionExtensions + { + [.(ExpectationMessage="to have length between {minLength} and {maxLength}")] + public static . HasLengthBetween(this string value, int minLength, int maxLength) { } + [.(ExpectationMessage="to have a maximum length of {maxLength}")] + public static . HasMaxLength(this string value, int maxLength) { } + [.(ExpectationMessage="to have a minimum length of {minLength}")] + public static . HasMinLength(this string value, int minLength) { } + } public class StringLengthValueAssertion : . { public StringLengthValueAssertion(. stringContext) { } @@ -5128,6 +5137,12 @@ namespace .Extensions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } + public static class StringLengthRangeAssertionExtensions + { + public static ._HasLengthBetween_Int_Int_Assertion HasLengthBetween(this . source, int minLength, int maxLength, [.("minLength")] string? minLengthExpression = null, [.("maxLength")] string? maxLengthExpression = null) { } + public static ._HasMaxLength_Int_Assertion HasMaxLength(this . source, int maxLength, [.("maxLength")] string? maxLengthExpression = null) { } + public static ._HasMinLength_Int_Assertion HasMinLength(this . source, int minLength, [.("minLength")] string? minLengthExpression = null) { } + } public static class StringStartsWithAssertionExtensions { public static . StartsWith(this . source, string expected, [.("expected")] string? expectedExpression = null) { } @@ -5140,6 +5155,24 @@ namespace .Extensions public static . IsNullOrEmpty(this . source) { } public static . IsNullOrWhiteSpace(this . source) { } } + public sealed class String_HasLengthBetween_Int_Int_Assertion : . + { + public String_HasLengthBetween_Int_Int_Assertion(. context, int minLength, int maxLength) { } + protected override .<.> CheckAsync(. metadata) { } + protected override string GetExpectation() { } + } + public sealed class String_HasMaxLength_Int_Assertion : . + { + public String_HasMaxLength_Int_Assertion(. context, int maxLength) { } + protected override .<.> CheckAsync(. metadata) { } + protected override string GetExpectation() { } + } + public sealed class String_HasMinLength_Int_Assertion : . + { + public String_HasMinLength_Int_Assertion(. context, int minLength) { } + protected override .<.> CheckAsync(. metadata) { } + protected override string GetExpectation() { } + } public sealed class String_IsEmpty_Assertion : . { public String_IsEmpty_Assertion(. context) { } 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 2600b47a79..68131a895b 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 @@ -2018,6 +2018,15 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } + public static class StringLengthRangeAssertionExtensions + { + [.(ExpectationMessage="to have length between {minLength} and {maxLength}")] + public static . HasLengthBetween(this string value, int minLength, int maxLength) { } + [.(ExpectationMessage="to have a maximum length of {maxLength}")] + public static . HasMaxLength(this string value, int maxLength) { } + [.(ExpectationMessage="to have a minimum length of {minLength}")] + public static . HasMinLength(this string value, int minLength) { } + } public class StringLengthValueAssertion : . { public StringLengthValueAssertion(. stringContext) { } @@ -5068,6 +5077,12 @@ namespace .Extensions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } + public static class StringLengthRangeAssertionExtensions + { + public static ._HasLengthBetween_Int_Int_Assertion HasLengthBetween(this . source, int minLength, int maxLength, [.("minLength")] string? minLengthExpression = null, [.("maxLength")] string? maxLengthExpression = null) { } + public static ._HasMaxLength_Int_Assertion HasMaxLength(this . source, int maxLength, [.("maxLength")] string? maxLengthExpression = null) { } + public static ._HasMinLength_Int_Assertion HasMinLength(this . source, int minLength, [.("minLength")] string? minLengthExpression = null) { } + } public static class StringStartsWithAssertionExtensions { public static . StartsWith(this . source, string expected, [.("expected")] string? expectedExpression = null) { } @@ -5080,6 +5095,24 @@ namespace .Extensions public static . IsNullOrEmpty(this . source) { } public static . IsNullOrWhiteSpace(this . source) { } } + public sealed class String_HasLengthBetween_Int_Int_Assertion : . + { + public String_HasLengthBetween_Int_Int_Assertion(. context, int minLength, int maxLength) { } + protected override .<.> CheckAsync(. metadata) { } + protected override string GetExpectation() { } + } + public sealed class String_HasMaxLength_Int_Assertion : . + { + public String_HasMaxLength_Int_Assertion(. context, int maxLength) { } + protected override .<.> CheckAsync(. metadata) { } + protected override string GetExpectation() { } + } + public sealed class String_HasMinLength_Int_Assertion : . + { + public String_HasMinLength_Int_Assertion(. context, int minLength) { } + protected override .<.> CheckAsync(. metadata) { } + protected override string GetExpectation() { } + } public sealed class String_IsEmpty_Assertion : . { public String_IsEmpty_Assertion(. context) { } 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 87b3a330a5..e896d0349d 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 @@ -2035,6 +2035,15 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } + public static class StringLengthRangeAssertionExtensions + { + [.(ExpectationMessage="to have length between {minLength} and {maxLength}")] + public static . HasLengthBetween(this string value, int minLength, int maxLength) { } + [.(ExpectationMessage="to have a maximum length of {maxLength}")] + public static . HasMaxLength(this string value, int maxLength) { } + [.(ExpectationMessage="to have a minimum length of {minLength}")] + public static . HasMinLength(this string value, int minLength) { } + } public class StringLengthValueAssertion : . { public StringLengthValueAssertion(. stringContext) { } @@ -5128,6 +5137,12 @@ namespace .Extensions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } + public static class StringLengthRangeAssertionExtensions + { + public static ._HasLengthBetween_Int_Int_Assertion HasLengthBetween(this . source, int minLength, int maxLength, [.("minLength")] string? minLengthExpression = null, [.("maxLength")] string? maxLengthExpression = null) { } + public static ._HasMaxLength_Int_Assertion HasMaxLength(this . source, int maxLength, [.("maxLength")] string? maxLengthExpression = null) { } + public static ._HasMinLength_Int_Assertion HasMinLength(this . source, int minLength, [.("minLength")] string? minLengthExpression = null) { } + } public static class StringStartsWithAssertionExtensions { public static . StartsWith(this . source, string expected, [.("expected")] string? expectedExpression = null) { } @@ -5140,6 +5155,24 @@ namespace .Extensions public static . IsNullOrEmpty(this . source) { } public static . IsNullOrWhiteSpace(this . source) { } } + public sealed class String_HasLengthBetween_Int_Int_Assertion : . + { + public String_HasLengthBetween_Int_Int_Assertion(. context, int minLength, int maxLength) { } + protected override .<.> CheckAsync(. metadata) { } + protected override string GetExpectation() { } + } + public sealed class String_HasMaxLength_Int_Assertion : . + { + public String_HasMaxLength_Int_Assertion(. context, int maxLength) { } + protected override .<.> CheckAsync(. metadata) { } + protected override string GetExpectation() { } + } + public sealed class String_HasMinLength_Int_Assertion : . + { + public String_HasMinLength_Int_Assertion(. context, int minLength) { } + protected override .<.> CheckAsync(. metadata) { } + protected override string GetExpectation() { } + } public sealed class String_IsEmpty_Assertion : . { public String_IsEmpty_Assertion(. context) { } 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 1fcf252a29..a88feff098 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 @@ -1811,6 +1811,15 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } + public static class StringLengthRangeAssertionExtensions + { + [.(ExpectationMessage="to have length between {minLength} and {maxLength}")] + public static . HasLengthBetween(this string value, int minLength, int maxLength) { } + [.(ExpectationMessage="to have a maximum length of {maxLength}")] + public static . HasMaxLength(this string value, int maxLength) { } + [.(ExpectationMessage="to have a minimum length of {minLength}")] + public static . HasMinLength(this string value, int minLength) { } + } public class StringLengthValueAssertion : . { public StringLengthValueAssertion(. stringContext) { } @@ -4415,6 +4424,12 @@ namespace .Extensions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } + public static class StringLengthRangeAssertionExtensions + { + public static ._HasLengthBetween_Int_Int_Assertion HasLengthBetween(this . source, int minLength, int maxLength, [.("minLength")] string? minLengthExpression = null, [.("maxLength")] string? maxLengthExpression = null) { } + public static ._HasMaxLength_Int_Assertion HasMaxLength(this . source, int maxLength, [.("maxLength")] string? maxLengthExpression = null) { } + public static ._HasMinLength_Int_Assertion HasMinLength(this . source, int minLength, [.("minLength")] string? minLengthExpression = null) { } + } public static class StringStartsWithAssertionExtensions { public static . StartsWith(this . source, string expected, [.("expected")] string? expectedExpression = null) { } @@ -4427,6 +4442,24 @@ namespace .Extensions public static . IsNullOrEmpty(this . source) { } public static . IsNullOrWhiteSpace(this . source) { } } + public sealed class String_HasLengthBetween_Int_Int_Assertion : . + { + public String_HasLengthBetween_Int_Int_Assertion(. context, int minLength, int maxLength) { } + protected override .<.> CheckAsync(. metadata) { } + protected override string GetExpectation() { } + } + public sealed class String_HasMaxLength_Int_Assertion : . + { + public String_HasMaxLength_Int_Assertion(. context, int maxLength) { } + protected override .<.> CheckAsync(. metadata) { } + protected override string GetExpectation() { } + } + public sealed class String_HasMinLength_Int_Assertion : . + { + public String_HasMinLength_Int_Assertion(. context, int minLength) { } + protected override .<.> CheckAsync(. metadata) { } + protected override string GetExpectation() { } + } public sealed class String_IsEmpty_Assertion : . { public String_IsEmpty_Assertion(. context) { } From 7995d5eadac76fa66f8f9e67dad21f16a2f1a103 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 23 Mar 2026 13:53:46 +0000 Subject: [PATCH 4/4] test: add tests for HasMinLength, HasMaxLength, and HasLengthBetween assertions Cover pass/fail cases, null input handling, bounds guard validation, and And-chaining for the new string length range assertions. --- .../StringLengthAssertionTests.cs | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/TUnit.Assertions.Tests/StringLengthAssertionTests.cs b/TUnit.Assertions.Tests/StringLengthAssertionTests.cs index 87fd7e2ae2..f93b5781cd 100644 --- a/TUnit.Assertions.Tests/StringLengthAssertionTests.cs +++ b/TUnit.Assertions.Tests/StringLengthAssertionTests.cs @@ -161,4 +161,123 @@ await Assert.That(str) .Length().IsGreaterThan(3) .And.IsLessThan(10); } + + [Test] + public async Task HasMinLength_Passes_When_At_Minimum() + { + await Assert.That("Hello").HasMinLength(5); + } + + [Test] + public async Task HasMinLength_Passes_When_Above_Minimum() + { + await Assert.That("Hello, World!").HasMinLength(5); + } + + [Test] + public async Task HasMinLength_Fails_When_Below_Minimum() + { + await Assert.That(async () => await Assert.That("Hi").HasMinLength(5)) + .Throws() + .And.HasMessageContaining("minimum length") + .And.HasMessageContaining("found length 2"); + } + + [Test] + public async Task HasMinLength_Fails_For_Null() + { + string? str = null; + await Assert.That(async () => await Assert.That(str!).HasMinLength(1)) + .Throws() + .And.HasMessageContaining("null"); + } + + [Test] + public async Task HasMaxLength_Passes_When_At_Maximum() + { + await Assert.That("Hello").HasMaxLength(5); + } + + [Test] + public async Task HasMaxLength_Passes_When_Below_Maximum() + { + await Assert.That("Hi").HasMaxLength(5); + } + + [Test] + public async Task HasMaxLength_Fails_When_Above_Maximum() + { + await Assert.That(async () => await Assert.That("Hello, World!").HasMaxLength(5)) + .Throws() + .And.HasMessageContaining("maximum length") + .And.HasMessageContaining("found length 13"); + } + + [Test] + public async Task HasMaxLength_Fails_For_Null() + { + string? str = null; + await Assert.That(async () => await Assert.That(str!).HasMaxLength(10)) + .Throws() + .And.HasMessageContaining("null"); + } + + [Test] + public async Task HasLengthBetween_Passes_When_Within_Range() + { + await Assert.That("Hello").HasLengthBetween(3, 10); + } + + [Test] + public async Task HasLengthBetween_Passes_When_At_Lower_Bound() + { + await Assert.That("Hello").HasLengthBetween(5, 10); + } + + [Test] + public async Task HasLengthBetween_Passes_When_At_Upper_Bound() + { + await Assert.That("Hello").HasLengthBetween(1, 5); + } + + [Test] + public async Task HasLengthBetween_Fails_When_Below_Range() + { + await Assert.That(async () => await Assert.That("Hi").HasLengthBetween(5, 10)) + .Throws() + .And.HasMessageContaining("between") + .And.HasMessageContaining("found length 2"); + } + + [Test] + public async Task HasLengthBetween_Fails_When_Above_Range() + { + await Assert.That(async () => await Assert.That("Hello, World!").HasLengthBetween(1, 5)) + .Throws() + .And.HasMessageContaining("between") + .And.HasMessageContaining("found length 13"); + } + + [Test] + public async Task HasLengthBetween_Fails_For_Null() + { + string? str = null; + await Assert.That(async () => await Assert.That(str!).HasLengthBetween(1, 10)) + .Throws() + .And.HasMessageContaining("null"); + } + + [Test] + public async Task HasLengthBetween_Throws_When_Min_Greater_Than_Max() + { + await Assert.That(async () => await Assert.That("Hello").HasLengthBetween(10, 3)) + .Throws() + .And.HasMessageContaining("minLength"); + } + + [Test] + public async Task HasMinLength_And_HasMaxLength_Chained() + { + await Assert.That("Hello").HasMinLength(3).And.HasMaxLength(10); + } }