From ca8d61f217757736ca1fcb425851baace997f1fd Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Sat, 1 Nov 2025 21:08:23 +0000
Subject: [PATCH 1/6] feat: update StringEqualsAssertion to support nullable
strings and add comprehensive tests for nullable string equality
---
.../NullableStringEqualityTests.cs | 151 ++++++++++++++++++
.../Conditions/StringEqualsAssertion.cs | 8 +-
...Has_No_API_Changes.DotNet10_0.verified.txt | 12 +-
..._Has_No_API_Changes.DotNet8_0.verified.txt | 12 +-
..._Has_No_API_Changes.DotNet9_0.verified.txt | 12 +-
...ary_Has_No_API_Changes.Net4_7.verified.txt | 12 +-
6 files changed, 179 insertions(+), 28 deletions(-)
create mode 100644 TUnit.Assertions.Tests/NullableStringEqualityTests.cs
diff --git a/TUnit.Assertions.Tests/NullableStringEqualityTests.cs b/TUnit.Assertions.Tests/NullableStringEqualityTests.cs
new file mode 100644
index 0000000000..666abd0ae6
--- /dev/null
+++ b/TUnit.Assertions.Tests/NullableStringEqualityTests.cs
@@ -0,0 +1,151 @@
+namespace TUnit.Assertions.Tests;
+
+///
+/// Tests to verify that IsEqualTo works correctly with nullable strings
+/// without requiring workarounds like `?? string.Empty`.
+/// Addresses GitHub issue #3643.
+///
+public class NullableStringEqualityTests
+{
+ [Test]
+ public async Task IsEqualTo_WithNullableStringActualAndNullableStringExpected_BothNonNull_Succeeds()
+ {
+ string? actualValue = "test";
+ string? expectedValue = "test";
+
+ // This should work without requiring `expectedValue ?? string.Empty`
+ await Assert.That(actualValue).IsEqualTo(expectedValue);
+ }
+
+ [Test]
+ public async Task IsEqualTo_WithNullableStringActualAndNullableStringExpected_BothNull_Succeeds()
+ {
+ string? actualValue = null;
+ string? expectedValue = null;
+
+ // Both null should be considered equal
+ await Assert.That(actualValue).IsEqualTo(expectedValue);
+ }
+
+ [Test]
+ public async Task IsEqualTo_WithNullableStringActualAndNullExpected_ActualNonNull_Fails()
+ {
+ string? actualValue = "test";
+ string? expectedValue = null;
+
+ await Assert.ThrowsAsync(async () =>
+ await Assert.That(actualValue).IsEqualTo(expectedValue));
+ }
+
+ [Test]
+ public async Task IsEqualTo_WithNullableStringActualAndNullExpected_ActualNull_Succeeds()
+ {
+ string? actualValue = null;
+ string? expectedValue = "test";
+
+ await Assert.ThrowsAsync(async () =>
+ await Assert.That(actualValue).IsEqualTo(expectedValue));
+ }
+
+ [Test]
+ public async Task IsEqualTo_WithNullableString_AndIgnoringCase_Succeeds()
+ {
+ string? actualValue = "TEST";
+ string? expectedValue = "test";
+
+ // Nullable strings should work with string-specific modifiers
+ await Assert.That(actualValue).IsEqualTo(expectedValue).IgnoringCase();
+ }
+
+ [Test]
+ public async Task IsEqualTo_WithNullableString_AndWithTrimming_Succeeds()
+ {
+ string? actualValue = " test ";
+ string? expectedValue = "test";
+
+ // Nullable strings should work with string-specific modifiers
+ await Assert.That(actualValue).IsEqualTo(expectedValue).WithTrimming();
+ }
+
+ [Test]
+ public async Task IsEqualTo_WithNullableString_AndIgnoringWhitespace_Succeeds()
+ {
+ string? actualValue = "t e s t";
+ string? expectedValue = "test";
+
+ // Nullable strings should work with string-specific modifiers
+ await Assert.That(actualValue).IsEqualTo(expectedValue).IgnoringWhitespace();
+ }
+
+ [Test]
+ public async Task IsEqualTo_WithNullableString_AndWithNullAndEmptyEquality_NullActualEmptyExpected_Succeeds()
+ {
+ string? actualValue = null;
+ string? expectedValue = "";
+
+ // null and empty should be considered equal with this modifier
+ await Assert.That(actualValue).IsEqualTo(expectedValue).WithNullAndEmptyEquality();
+ }
+
+ [Test]
+ public async Task IsEqualTo_WithNullableString_AndWithNullAndEmptyEquality_EmptyActualNullExpected_Succeeds()
+ {
+ string? actualValue = "";
+ string? expectedValue = null;
+
+ // null and empty should be considered equal with this modifier
+ await Assert.That(actualValue).IsEqualTo(expectedValue).WithNullAndEmptyEquality();
+ }
+
+ [Test]
+ public async Task IsEqualTo_WithNullableString_AndWithNullAndEmptyEquality_BothNull_Succeeds()
+ {
+ string? actualValue = null;
+ string? expectedValue = null;
+
+ await Assert.That(actualValue).IsEqualTo(expectedValue).WithNullAndEmptyEquality();
+ }
+
+ [Test]
+ public async Task IsEqualTo_WithNullableString_AndWithComparison_Succeeds()
+ {
+ string? actualValue = "TEST";
+ string? expectedValue = "test";
+
+ // Nullable strings should work with StringComparison parameter
+ await Assert.That(actualValue).IsEqualTo(expectedValue).WithComparison(StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Test]
+ public async Task IsEqualTo_WithNullableString_CombinedModifiers_Succeeds()
+ {
+ string? actualValue = " T E S T ";
+ string? expectedValue = "test";
+
+ // Multiple modifiers should work together with nullable strings
+ await Assert.That(actualValue).IsEqualTo(expectedValue).WithTrimming().IgnoringWhitespace().IgnoringCase();
+ }
+
+ [Test]
+ public async Task IsEqualTo_WithNullableVariableDeclaredWithVar_Succeeds()
+ {
+ // Verify it works with var (type inference) as well
+ string? actual = GetNullableString();
+ string? expected = "result";
+
+ await Assert.That(actual).IsEqualTo(expected);
+ }
+
+ [Test]
+ public async Task IsEqualTo_WithNullableStringFromMethod_BothNull_Succeeds()
+ {
+ string? actual = GetNullString();
+ string? expected = GetNullString();
+
+ await Assert.That(actual).IsEqualTo(expected);
+ }
+
+ private static string? GetNullableString() => "result";
+
+ private static string? GetNullString() => null;
+}
diff --git a/TUnit.Assertions/Conditions/StringEqualsAssertion.cs b/TUnit.Assertions/Conditions/StringEqualsAssertion.cs
index 99ee7e6af4..ab0a6ecb08 100644
--- a/TUnit.Assertions/Conditions/StringEqualsAssertion.cs
+++ b/TUnit.Assertions/Conditions/StringEqualsAssertion.cs
@@ -9,7 +9,7 @@ namespace TUnit.Assertions.Conditions;
/// Demonstrates multiple custom methods WITHOUT wrappers!
///
[AssertionExtension("IsEqualTo", OverloadResolutionPriority = 2)]
-public class StringEqualsAssertion : Assertion
+public class StringEqualsAssertion : Assertion
{
private readonly string? _expected;
private StringComparison _comparison = StringComparison.Ordinal;
@@ -18,7 +18,7 @@ public class StringEqualsAssertion : Assertion
private bool _ignoringWhitespace;
public StringEqualsAssertion(
- AssertionContext context,
+ AssertionContext context,
string? expected)
: base(context)
{
@@ -26,7 +26,7 @@ public StringEqualsAssertion(
}
public StringEqualsAssertion(
- AssertionContext context,
+ AssertionContext context,
string? expected,
StringComparison comparison)
: base(context)
@@ -85,7 +85,7 @@ public StringEqualsAssertion IgnoringWhitespace()
return this;
}
- protected override Task CheckAsync(EvaluationMetadata metadata)
+ protected override Task CheckAsync(EvaluationMetadata metadata)
{
var value = metadata.Value;
var exception = metadata.Exception;
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 ff4415bf1a..6d98f6861c 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
@@ -1071,11 +1071,11 @@ namespace .Conditions
public . WithComparison( comparison) { }
}
[.("IsEqualTo", OverloadResolutionPriority=2)]
- public class StringEqualsAssertion : .
+ public class StringEqualsAssertion : .
{
- public StringEqualsAssertion(. context, string? expected) { }
- public StringEqualsAssertion(. context, string? expected, comparison) { }
- protected override .<.> CheckAsync(. metadata) { }
+ public StringEqualsAssertion(. context, string? expected) { }
+ public StringEqualsAssertion(. context, string? expected, comparison) { }
+ protected override .<.> CheckAsync(. metadata) { }
protected override string GetExpectation() { }
public . IgnoringCase() { }
public . IgnoringWhitespace() { }
@@ -3565,9 +3565,9 @@ namespace .Extensions
public static class StringEqualsAssertionExtensions
{
[.(2)]
- public static . IsEqualTo(this . source, string? expected, [.("expected")] string? expectedExpression = null) { }
+ public static . IsEqualTo(this . source, string? expected, [.("expected")] string? expectedExpression = null) { }
[.(2)]
- public static . IsEqualTo(this . source, string? expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { }
+ public static . IsEqualTo(this . source, string? expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { }
}
public static class StringIsEmptyAssertionExtensions
{
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 4724baca2c..5cc3222ee9 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
@@ -1068,11 +1068,11 @@ namespace .Conditions
public . WithComparison( comparison) { }
}
[.("IsEqualTo", OverloadResolutionPriority=2)]
- public class StringEqualsAssertion : .
+ public class StringEqualsAssertion : .
{
- public StringEqualsAssertion(. context, string? expected) { }
- public StringEqualsAssertion(. context, string? expected, comparison) { }
- protected override .<.> CheckAsync(. metadata) { }
+ public StringEqualsAssertion(. context, string? expected) { }
+ public StringEqualsAssertion(. context, string? expected, comparison) { }
+ protected override .<.> CheckAsync(. metadata) { }
protected override string GetExpectation() { }
public . IgnoringCase() { }
public . IgnoringWhitespace() { }
@@ -3546,8 +3546,8 @@ namespace .Extensions
}
public static class StringEqualsAssertionExtensions
{
- public static . IsEqualTo(this . source, string? expected, [.("expected")] string? expectedExpression = null) { }
- public static . IsEqualTo(this . source, string? expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { }
+ public static . IsEqualTo(this . source, string? expected, [.("expected")] string? expectedExpression = null) { }
+ public static . IsEqualTo(this . source, string? expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { }
}
public static class StringIsEmptyAssertionExtensions
{
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 aef5215e73..ca9170a7aa 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
@@ -1071,11 +1071,11 @@ namespace .Conditions
public . WithComparison( comparison) { }
}
[.("IsEqualTo", OverloadResolutionPriority=2)]
- public class StringEqualsAssertion : .
+ public class StringEqualsAssertion : .
{
- public StringEqualsAssertion(. context, string? expected) { }
- public StringEqualsAssertion(. context, string? expected, comparison) { }
- protected override .<.> CheckAsync(. metadata) { }
+ public StringEqualsAssertion(. context, string? expected) { }
+ public StringEqualsAssertion(. context, string? expected, comparison) { }
+ protected override .<.> CheckAsync(. metadata) { }
protected override string GetExpectation() { }
public . IgnoringCase() { }
public . IgnoringWhitespace() { }
@@ -3565,9 +3565,9 @@ namespace .Extensions
public static class StringEqualsAssertionExtensions
{
[.(2)]
- public static . IsEqualTo(this . source, string? expected, [.("expected")] string? expectedExpression = null) { }
+ public static . IsEqualTo(this . source, string? expected, [.("expected")] string? expectedExpression = null) { }
[.(2)]
- public static . IsEqualTo(this . source, string? expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { }
+ public static . IsEqualTo(this . source, string? expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { }
}
public static class StringIsEmptyAssertionExtensions
{
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 94d42ce41d..47c892ea72 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
@@ -998,11 +998,11 @@ namespace .Conditions
public . WithComparison( comparison) { }
}
[.("IsEqualTo", OverloadResolutionPriority=2)]
- public class StringEqualsAssertion : .
+ public class StringEqualsAssertion : .
{
- public StringEqualsAssertion(. context, string? expected) { }
- public StringEqualsAssertion(. context, string? expected, comparison) { }
- protected override .<.> CheckAsync(. metadata) { }
+ public StringEqualsAssertion(. context, string? expected) { }
+ public StringEqualsAssertion(. context, string? expected, comparison) { }
+ protected override .<.> CheckAsync(. metadata) { }
protected override string GetExpectation() { }
public . IgnoringCase() { }
public . IgnoringWhitespace() { }
@@ -3116,8 +3116,8 @@ namespace .Extensions
}
public static class StringEqualsAssertionExtensions
{
- public static . IsEqualTo(this . source, string? expected, [.("expected")] string? expectedExpression = null) { }
- public static . IsEqualTo(this . source, string? expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { }
+ public static . IsEqualTo(this . source, string? expected, [.("expected")] string? expectedExpression = null) { }
+ public static . IsEqualTo(this . source, string? expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { }
}
public static class StringIsEmptyAssertionExtensions
{
From 11777ef5d22cf6138bc01bb817d7b4aa93fabe29 Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Sat, 1 Nov 2025 22:32:51 +0000
Subject: [PATCH 2/6] feat: add comprehensive tests for nullable string
equality to prevent nullability warnings
---
.../Generators/AssertionExtensionGenerator.cs | 7 +-
.../NullabilityWarningTests.cs | 135 ++++++++++++++++++
2 files changed, 138 insertions(+), 4 deletions(-)
create mode 100644 TUnit.Assertions.Tests/NullabilityWarningTests.cs
diff --git a/TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs b/TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs
index 0c42f0d347..bb56a0c79d 100644
--- a/TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs
+++ b/TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs
@@ -316,7 +316,7 @@ private static void GenerateExtensionMethod(
sourceBuilder.AppendLine($" [global::System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode(\"{escapedMessage}\")]");
}
- // Add OverloadResolutionPriority attribute only if priority > 0
+ // Add OverloadResolutionPriority attribute if specified
if (data.OverloadResolutionPriority > 0)
{
sourceBuilder.AppendLine($" [global::System.Runtime.CompilerServices.OverloadResolutionPriority({data.OverloadResolutionPriority})]");
@@ -327,9 +327,8 @@ private static void GenerateExtensionMethod(
? $"{assertionType.Name}{genericParamsString}"
: assertionType.Name;
- // The extension method always extends IAssertionSource where T is the type argument
- // from the Assertion base class. This ensures the source.Context type matches what
- // the assertion constructor expects.
+ // The extension method extends IAssertionSource where T is the type argument
+ // from the Assertion base class.
string sourceType;
if (typeParam is ITypeParameterSymbol baseTypeParam)
{
diff --git a/TUnit.Assertions.Tests/NullabilityWarningTests.cs b/TUnit.Assertions.Tests/NullabilityWarningTests.cs
new file mode 100644
index 0000000000..7a99bbe965
--- /dev/null
+++ b/TUnit.Assertions.Tests/NullabilityWarningTests.cs
@@ -0,0 +1,135 @@
+namespace TUnit.Assertions.Tests;
+
+///
+/// Tests to ensure that all combinations of nullable/non-nullable strings
+/// work without generating nullability warnings (CS8604, CS8625, etc.)
+/// This validates the fix for GitHub issue #3643.
+///
+public class NullabilityWarningTests
+{
+ [Test]
+ public async Task NonNullableString_WithNonNullableExpected_NoWarning()
+ {
+ string actualValue = "test";
+ string expectedValue = "test";
+
+ // Non-nullable to non-nullable should work
+ await Assert.That(actualValue).IsEqualTo(expectedValue);
+ }
+
+ [Test]
+ public async Task NonNullableString_WithNullableExpected_NoWarning()
+ {
+ string actualValue = "test";
+ string? expectedValue = "test";
+
+ // Non-nullable string should accept nullable expected without warning
+ await Assert.That(actualValue).IsEqualTo(expectedValue);
+ }
+
+ [Test]
+ public async Task NullableString_WithNonNullableExpected_NoWarning()
+ {
+ string? actualValue = "test";
+ string expectedValue = "test";
+
+ // Nullable string should accept non-nullable expected without warning
+ await Assert.That(actualValue).IsEqualTo(expectedValue);
+ }
+
+ [Test]
+ public async Task NullableString_WithNullableExpected_NoWarning()
+ {
+ string? actualValue = "test";
+ string? expectedValue = "test";
+
+ // Nullable to nullable should work (this is the main issue from #3643)
+ await Assert.That(actualValue).IsEqualTo(expectedValue);
+ }
+
+ [Test]
+ public async Task NonNullableString_WithNullLiteral_NoWarning()
+ {
+ string actualValue = "test";
+
+ // Passing null literal should be allowed (tests for null mismatch)
+ await Assert.ThrowsAsync(async () =>
+ await Assert.That(actualValue).IsEqualTo(null));
+ }
+
+ [Test]
+ public async Task NullableString_WithNullLiteral_NoWarning()
+ {
+ string? actualValue = null;
+
+ // Nullable string with null literal should work
+ await Assert.That(actualValue).IsEqualTo(null);
+ }
+
+ [Test]
+ public async Task NonNullableString_WithStringComparison_NoWarning()
+ {
+ string actualValue = "TEST";
+ string? expectedValue = "test";
+
+ // Non-nullable with StringComparison parameter and nullable expected
+ await Assert.That(actualValue).IsEqualTo(expectedValue, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Test]
+ public async Task NullableString_WithStringComparison_NoWarning()
+ {
+ string? actualValue = "TEST";
+ string? expectedValue = "test";
+
+ // Nullable with StringComparison parameter
+ await Assert.That(actualValue).IsEqualTo(expectedValue, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Test]
+ public async Task NonNullableString_WithModifiers_NoWarning()
+ {
+ string actualValue = " TEST ";
+ string? expectedValue = "test";
+
+ // Non-nullable with modifiers and nullable expected
+ await Assert.That(actualValue).IsEqualTo(expectedValue).WithTrimming().IgnoringCase();
+ }
+
+ [Test]
+ public async Task NullableString_WithModifiers_NoWarning()
+ {
+ string? actualValue = " TEST ";
+ string? expectedValue = "test";
+
+ // Nullable with modifiers
+ await Assert.That(actualValue).IsEqualTo(expectedValue).WithTrimming().IgnoringCase();
+ }
+
+ [Test]
+ public async Task MixedNullability_FromMethods_NoWarning()
+ {
+ // Testing that method return values work correctly
+ string nonNullable = GetNonNullableString();
+ string? nullable = GetNullableString();
+
+ await Assert.That(nonNullable).IsEqualTo(nullable);
+ await Assert.That(nullable).IsEqualTo(nonNullable);
+ }
+
+ [Test]
+ public async Task ImplicitConversion_NonNullableToNullable_NoWarning()
+ {
+ // This verifies that the generated extension method signature
+ // IAssertionSource accepts both string and string? without warnings
+ string nonNullable = "test";
+
+ // The implicit conversion from string to string? should work seamlessly
+ await Assert.That(nonNullable).IsEqualTo("test");
+ await Assert.That(nonNullable).IsEqualTo((string?)"test");
+ }
+
+ private static string GetNonNullableString() => "result";
+
+ private static string? GetNullableString() => "result";
+}
From b39c3625c8be670ee1ce048c3283f408a75133d1 Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Sat, 1 Nov 2025 22:48:21 +0000
Subject: [PATCH 3/6] feat: add nullable overloads for assertion methods to
support non-nullable reference types
---
.../Generators/AssertionExtensionGenerator.cs | 85 ++++++++++++++++---
TUnit.Assertions/Core/AssertionContext.cs | 16 ++++
...Has_No_API_Changes.DotNet10_0.verified.txt | 6 ++
..._Has_No_API_Changes.DotNet8_0.verified.txt | 4 +
..._Has_No_API_Changes.DotNet9_0.verified.txt | 6 ++
...ary_Has_No_API_Changes.Net4_7.verified.txt | 4 +
6 files changed, 110 insertions(+), 11 deletions(-)
diff --git a/TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs b/TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs
index bb56a0c79d..70a3099507 100644
--- a/TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs
+++ b/TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs
@@ -170,13 +170,30 @@ private static void GenerateExtensionMethods(SourceProductionContext context, As
continue;
}
- // Generate positive assertion method
- GenerateExtensionMethod(sourceBuilder, data, constructor, negated: false);
+ // Check if the type parameter is a nullable reference type (e.g., string?)
+ var typeParam = data.AssertionBaseType.TypeArguments[0];
+ var isNullableReferenceType = typeParam.NullableAnnotation == NullableAnnotation.Annotated &&
+ typeParam.IsReferenceType;
+
+ // Generate positive assertion method for nullable source
+ GenerateExtensionMethod(sourceBuilder, data, constructor, negated: false, isNullableOverload: false);
+
+ // If nullable reference type, also generate overload for non-nullable source
+ // This uses a generic constraint to create a distinct signature
+ if (isNullableReferenceType)
+ {
+ GenerateExtensionMethod(sourceBuilder, data, constructor, negated: false, isNullableOverload: true);
+ }
// Generate negated assertion method if requested
if (!string.IsNullOrEmpty(data.NegatedMethodName))
{
- GenerateExtensionMethod(sourceBuilder, data, constructor, negated: true);
+ GenerateExtensionMethod(sourceBuilder, data, constructor, negated: true, isNullableOverload: false);
+
+ if (isNullableReferenceType)
+ {
+ GenerateExtensionMethod(sourceBuilder, data, constructor, negated: true, isNullableOverload: true);
+ }
}
}
@@ -211,7 +228,8 @@ private static void GenerateExtensionMethod(
StringBuilder sourceBuilder,
AssertionExtensionData data,
IMethodSymbol constructor,
- bool negated)
+ bool negated,
+ bool isNullableOverload)
{
var methodName = negated ? data.NegatedMethodName : data.MethodName;
var assertionType = data.ClassSymbol;
@@ -317,9 +335,17 @@ private static void GenerateExtensionMethod(
}
// Add OverloadResolutionPriority attribute if specified
- if (data.OverloadResolutionPriority > 0)
+ // For nullable overloads (generic with class constraint), increase priority by 1
+ // so they're preferred over the base nullable overload when source is non-nullable
+ var effectivePriority = data.OverloadResolutionPriority;
+ if (isNullableOverload)
+ {
+ effectivePriority += 1;
+ }
+
+ if (effectivePriority > 0)
{
- sourceBuilder.AppendLine($" [global::System.Runtime.CompilerServices.OverloadResolutionPriority({data.OverloadResolutionPriority})]");
+ sourceBuilder.AppendLine($" [global::System.Runtime.CompilerServices.OverloadResolutionPriority({effectivePriority})]");
}
// Method declaration
@@ -330,7 +356,19 @@ private static void GenerateExtensionMethod(
// The extension method extends IAssertionSource where T is the type argument
// from the Assertion base class.
string sourceType;
- if (typeParam is ITypeParameterSymbol baseTypeParam)
+ string genericTypeParam = null;
+ string genericConstraint = null;
+
+ if (isNullableOverload)
+ {
+ // For nullable overload, use a generic type parameter with class constraint
+ // e.g., IsEqualTo(this IAssertionSource source, string? expected) where TNonNullable : class
+ // We can't constrain to 'string' directly since it's a sealed type, but 'class' ensures it's a reference type
+ genericTypeParam = "TNonNullable";
+ sourceType = $"IAssertionSource<{genericTypeParam}>";
+ genericConstraint = $"where {genericTypeParam} : class";
+ }
+ else if (typeParam is ITypeParameterSymbol baseTypeParam)
{
sourceType = $"IAssertionSource<{baseTypeParam.Name}>";
}
@@ -339,7 +377,13 @@ private static void GenerateExtensionMethod(
sourceType = $"IAssertionSource<{typeParam.ToDisplayString()}>";
}
- sourceBuilder.Append($" public static {returnType} {methodName}{genericParamsString}(");
+ sourceBuilder.Append($" public static {returnType} {methodName}");
+ if (genericTypeParam != null)
+ {
+ sourceBuilder.Append($"<{genericTypeParam}>");
+ }
+ sourceBuilder.Append(genericParamsString);
+ sourceBuilder.Append("(");
sourceBuilder.Append($"this {sourceType} source");
// Add additional parameters
@@ -365,10 +409,16 @@ private static void GenerateExtensionMethod(
sourceBuilder.Append(")");
// Add type constraints on new line if any
- if (typeConstraints.Count > 0)
+ var allConstraints = new List(typeConstraints);
+ if (genericConstraint != null)
+ {
+ allConstraints.Add(genericConstraint);
+ }
+
+ if (allConstraints.Count > 0)
{
sourceBuilder.AppendLine();
- sourceBuilder.Append($" {string.Join(" ", typeConstraints)}");
+ sourceBuilder.Append($" {string.Join(" ", allConstraints)}");
}
sourceBuilder.AppendLine();
@@ -393,7 +443,20 @@ private static void GenerateExtensionMethod(
{
sourceBuilder.Append($"<{string.Join(", ", genericParams)}>");
}
- sourceBuilder.Append("(source.Context");
+ sourceBuilder.Append("(");
+
+ // For non-nullable source with nullable assertion, use AsNullable() to convert the context
+ if (isNullableOverload)
+ {
+ // AsNullable() safely converts AssertionContext to AssertionContext
+ // We then cast to the target nullable type through object
+ var nullableTypeName = typeParam.ToDisplayString();
+ sourceBuilder.Append($"(AssertionContext<{nullableTypeName}>)(object)source.Context.AsNullable()");
+ }
+ else
+ {
+ sourceBuilder.Append("source.Context");
+ }
foreach (var param in additionalParams)
{
diff --git a/TUnit.Assertions/Core/AssertionContext.cs b/TUnit.Assertions/Core/AssertionContext.cs
index 09ab90bc3b..7c7157f31b 100644
--- a/TUnit.Assertions/Core/AssertionContext.cs
+++ b/TUnit.Assertions/Core/AssertionContext.cs
@@ -153,4 +153,20 @@ internal void SetPendingLink(Assertion previous, CombinerType type)
PendingLinkType = null;
return result;
}
+
+ ///
+ /// Converts a non-nullable reference type context to its nullable equivalent.
+ /// This is safe because reference types and their nullable counterparts have identical runtime representations.
+ /// Used primarily by the assertion source generator to handle non-nullable to nullable conversions.
+ /// IMPORTANT: This should only be called when TValue is a reference type (class).
+ ///
+ /// The same context instance viewed as nullable
+ internal AssertionContext AsNullable()
+ {
+ // This cast is safe because for reference types, TValue and TValue?
+ // have the same runtime representation. We are only changing the
+ // compiler's static analysis view of the type.
+ // The generator ensures this is only called for reference types.
+ return (AssertionContext)(object)this;
+ }
}
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 6d98f6861c..bb6caf4a1a 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
@@ -3568,6 +3568,12 @@ namespace .Extensions
public static . IsEqualTo(this . source, string? expected, [.("expected")] string? expectedExpression = null) { }
[.(2)]
public static . IsEqualTo(this . source, string? expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { }
+ [.(3)]
+ public static . IsEqualTo(this . source, string? expected, [.("expected")] string? expectedExpression = null)
+ where TNonNullable : class { }
+ [.(3)]
+ public static . IsEqualTo(this . source, string? expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null)
+ where TNonNullable : class { }
}
public static class StringIsEmptyAssertionExtensions
{
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 5cc3222ee9..d8018896c4 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
@@ -3548,6 +3548,10 @@ namespace .Extensions
{
public static . IsEqualTo(this . source, string? expected, [.("expected")] string? expectedExpression = null) { }
public static . IsEqualTo(this . source, string? expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { }
+ public static . IsEqualTo(this . source, string? expected, [.("expected")] string? expectedExpression = null)
+ where TNonNullable : class { }
+ public static . IsEqualTo(this . source, string? expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null)
+ where TNonNullable : class { }
}
public static class StringIsEmptyAssertionExtensions
{
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 ca9170a7aa..cac2d5444c 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
@@ -3568,6 +3568,12 @@ namespace .Extensions
public static . IsEqualTo(this . source, string? expected, [.("expected")] string? expectedExpression = null) { }
[.(2)]
public static . IsEqualTo(this . source, string? expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { }
+ [.(3)]
+ public static . IsEqualTo(this . source, string? expected, [.("expected")] string? expectedExpression = null)
+ where TNonNullable : class { }
+ [.(3)]
+ public static . IsEqualTo(this . source, string? expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null)
+ where TNonNullable : class { }
}
public static class StringIsEmptyAssertionExtensions
{
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 47c892ea72..aa789906e2 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
@@ -3118,6 +3118,10 @@ namespace .Extensions
{
public static . IsEqualTo(this . source, string? expected, [.("expected")] string? expectedExpression = null) { }
public static . IsEqualTo(this . source, string? expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { }
+ public static . IsEqualTo(this . source, string? expected, [.("expected")] string? expectedExpression = null)
+ where TNonNullable : class { }
+ public static . IsEqualTo(this . source, string? expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null)
+ where TNonNullable : class { }
}
public static class StringIsEmptyAssertionExtensions
{
From 26371de38a8862b33163b5e807793f7bf6f89df7 Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Sun, 2 Nov 2025 00:12:22 +0000
Subject: [PATCH 4/6] feat: normalize line endings in exception messages for
consistent cross-platform testing
---
.../Generators/AssertionExtensionGenerator.cs | 32 +++++------------
.../AssertConditions/BecauseTests.cs | 2 +-
.../Delegates/Throws.ExactlyTests.cs | 6 ++--
TUnit.Assertions.Tests/Bugs/Tests2145.cs | 13 +++----
TUnit.Assertions.Tests/GlobalUsings.cs | 11 ++++++
.../Old/AssertMultipleTests.cs | 34 +++++++++----------
.../Old/EquivalentAssertionTests.cs | 18 +++++-----
.../Old/StringEqualsAssertionTests.cs | 2 +-
...Has_No_API_Changes.DotNet10_0.verified.txt | 6 ----
..._Has_No_API_Changes.DotNet8_0.verified.txt | 4 ---
..._Has_No_API_Changes.DotNet9_0.verified.txt | 6 ----
...ary_Has_No_API_Changes.Net4_7.verified.txt | 4 ---
12 files changed, 58 insertions(+), 80 deletions(-)
diff --git a/TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs b/TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs
index 70a3099507..8cb0b1debb 100644
--- a/TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs
+++ b/TUnit.Assertions.SourceGenerator/Generators/AssertionExtensionGenerator.cs
@@ -175,25 +175,13 @@ private static void GenerateExtensionMethods(SourceProductionContext context, As
var isNullableReferenceType = typeParam.NullableAnnotation == NullableAnnotation.Annotated &&
typeParam.IsReferenceType;
- // Generate positive assertion method for nullable source
+ // Generate positive assertion method
GenerateExtensionMethod(sourceBuilder, data, constructor, negated: false, isNullableOverload: false);
- // If nullable reference type, also generate overload for non-nullable source
- // This uses a generic constraint to create a distinct signature
- if (isNullableReferenceType)
- {
- GenerateExtensionMethod(sourceBuilder, data, constructor, negated: false, isNullableOverload: true);
- }
-
// Generate negated assertion method if requested
if (!string.IsNullOrEmpty(data.NegatedMethodName))
{
GenerateExtensionMethod(sourceBuilder, data, constructor, negated: true, isNullableOverload: false);
-
- if (isNullableReferenceType)
- {
- GenerateExtensionMethod(sourceBuilder, data, constructor, negated: true, isNullableOverload: true);
- }
}
}
@@ -361,12 +349,12 @@ private static void GenerateExtensionMethod(
if (isNullableOverload)
{
- // For nullable overload, use a generic type parameter with class constraint
- // e.g., IsEqualTo(this IAssertionSource source, string? expected) where TNonNullable : class
- // We can't constrain to 'string' directly since it's a sealed type, but 'class' ensures it's a reference type
- genericTypeParam = "TNonNullable";
- sourceType = $"IAssertionSource<{genericTypeParam}>";
- genericConstraint = $"where {genericTypeParam} : class";
+ // For nullable reference types, we can't use two separate overloads for T and T?
+ // because NRT annotations are erased at runtime - they're the same type to the CLR.
+ // Instead, just use the nullable version and accept both nullable and non-nullable sources.
+ sourceType = $"IAssertionSource<{typeParam.ToDisplayString()}>";
+ genericTypeParam = null;
+ genericConstraint = null;
}
else if (typeParam is ITypeParameterSymbol baseTypeParam)
{
@@ -445,13 +433,9 @@ private static void GenerateExtensionMethod(
}
sourceBuilder.Append("(");
- // For non-nullable source with nullable assertion, use AsNullable() to convert the context
if (isNullableOverload)
{
- // AsNullable() safely converts AssertionContext to AssertionContext
- // We then cast to the target nullable type through object
- var nullableTypeName = typeParam.ToDisplayString();
- sourceBuilder.Append($"(AssertionContext<{nullableTypeName}>)(object)source.Context.AsNullable()");
+ sourceBuilder.Append("source.Context.AsNullable()");
}
else
{
diff --git a/TUnit.Assertions.Tests/AssertConditions/BecauseTests.cs b/TUnit.Assertions.Tests/AssertConditions/BecauseTests.cs
index 0193dc0403..1d44b17e48 100644
--- a/TUnit.Assertions.Tests/AssertConditions/BecauseTests.cs
+++ b/TUnit.Assertions.Tests/AssertConditions/BecauseTests.cs
@@ -91,7 +91,7 @@ await Assert.That(variable).IsTrue().Because(because)
};
var exception = await Assert.ThrowsAsync(action);
- await Assert.That(exception.Message).IsEqualTo(expectedMessage);
+ await Assert.That(exception.Message.NormalizeLineEndings()).IsEqualTo(expectedMessage);
}
[Test]
diff --git a/TUnit.Assertions.Tests/Assertions/Delegates/Throws.ExactlyTests.cs b/TUnit.Assertions.Tests/Assertions/Delegates/Throws.ExactlyTests.cs
index 82fbb1f190..c3292ec78e 100644
--- a/TUnit.Assertions.Tests/Assertions/Delegates/Throws.ExactlyTests.cs
+++ b/TUnit.Assertions.Tests/Assertions/Delegates/Throws.ExactlyTests.cs
@@ -140,10 +140,12 @@ public async Task Conversion_To_Value_Assertion_Builder_On_Casted_Exception_Type
await Assert.That((object)ex).IsAssignableTo();
});
- await Assert.That(assertionException).HasMessageStartingWith("""
+ var expectedPrefix = """
Expected to throw exactly CustomException
and to have message equal to "Foo bar message!"
- """);
+ """;
+
+ await Assert.That(assertionException.Message.NormalizeLineEndings()).StartsWith(expectedPrefix.NormalizeLineEndings());
}
}
}
diff --git a/TUnit.Assertions.Tests/Bugs/Tests2145.cs b/TUnit.Assertions.Tests/Bugs/Tests2145.cs
index a95f255b79..1112cc6b4a 100644
--- a/TUnit.Assertions.Tests/Bugs/Tests2145.cs
+++ b/TUnit.Assertions.Tests/Bugs/Tests2145.cs
@@ -5,16 +5,16 @@ public class Tests2145
[Test]
public async Task TestFailMessage()
{
- await Assert.That(async () =>
+ var exception = await Assert.ThrowsAsync(async () =>
{
var val = "hello";
using var _ = Assert.Multiple();
await Assert.That(val).IsEqualTo("world");
await Assert.That(val).IsEqualTo("World");
- }).Throws()
- .WithMessage(
- """
+ });
+
+ var expectedMessage = """
Expected to be equal to "world"
but found "hello"
@@ -24,7 +24,8 @@ Expected to be equal to "World"
but found "hello"
at Assert.That(val).IsEqualTo("World")
- """
- );
+ """;
+
+ await Assert.That(exception!.Message.NormalizeLineEndings()).IsEqualTo(expectedMessage.NormalizeLineEndings());
}
}
diff --git a/TUnit.Assertions.Tests/GlobalUsings.cs b/TUnit.Assertions.Tests/GlobalUsings.cs
index 4d93167cd3..1af00b83b7 100644
--- a/TUnit.Assertions.Tests/GlobalUsings.cs
+++ b/TUnit.Assertions.Tests/GlobalUsings.cs
@@ -1,3 +1,14 @@
global using TUnit.Assertions.Exceptions;
global using TUnit.Assertions.Extensions;
global using TUnit.Core;
+
+internal static class TestHelpers
+{
+ ///
+ /// Normalizes all line endings to Unix-style (\n) for consistent cross-platform testing.
+ ///
+ public static string NormalizeLineEndings(this string value)
+ {
+ return value.Replace("\r\n", "\n").Replace("\r", "\n");
+ }
+}
diff --git a/TUnit.Assertions.Tests/Old/AssertMultipleTests.cs b/TUnit.Assertions.Tests/Old/AssertMultipleTests.cs
index e1fcd0b335..a12e4a5421 100644
--- a/TUnit.Assertions.Tests/Old/AssertMultipleTests.cs
+++ b/TUnit.Assertions.Tests/Old/AssertMultipleTests.cs
@@ -28,35 +28,35 @@ public async Task MultipleFailures()
var exception4 = (TUnitAssertionException) aggregateException.InnerExceptions[3];
var exception5 = (TUnitAssertionException) aggregateException.InnerExceptions[4];
- await TUnitAssert.That(exception1.Message).IsEqualTo("""
+ await TUnitAssert.That(exception1.Message.NormalizeLineEndings()).IsEqualTo("""
Expected to be 2
but found 1
at Assert.That(1).IsEqualTo(2)
""");
- await TUnitAssert.That(exception2.Message).IsEqualTo("""
+ await TUnitAssert.That(exception2.Message.NormalizeLineEndings()).IsEqualTo("""
Expected to be 3
but found 2
at Assert.That(2).IsEqualTo(3)
""");
- await TUnitAssert.That(exception3.Message).IsEqualTo("""
+ await TUnitAssert.That(exception3.Message.NormalizeLineEndings()).IsEqualTo("""
Expected to be 4
but found 3
at Assert.That(3).IsEqualTo(4)
""");
- await TUnitAssert.That(exception4.Message).IsEqualTo("""
+ await TUnitAssert.That(exception4.Message.NormalizeLineEndings()).IsEqualTo("""
Expected to be 5
but found 4
at Assert.That(4).IsEqualTo(5)
""");
- await TUnitAssert.That(exception5.Message).IsEqualTo("""
+ await TUnitAssert.That(exception5.Message.NormalizeLineEndings()).IsEqualTo("""
Expected to be 6
but found 5
@@ -87,7 +87,7 @@ public async Task MultipleFailures_With_Connectors()
var exception4 = (TUnitAssertionException) aggregateException.InnerExceptions[3];
var exception5 = (TUnitAssertionException) aggregateException.InnerExceptions[4];
- await TUnitAssert.That(exception1.Message).IsEqualTo("""
+ await TUnitAssert.That(exception1.Message.NormalizeLineEndings()).IsEqualTo("""
Expected to be 2
or to be 3
but found 1
@@ -95,7 +95,7 @@ but found 1
at Assert.That(1).IsEqualTo(2).Or.IsEqualTo(3)
""");
- await TUnitAssert.That(exception2.Message).IsEqualTo("""
+ await TUnitAssert.That(exception2.Message.NormalizeLineEndings()).IsEqualTo("""
Expected to be 3
and to be 4
but found 2
@@ -103,7 +103,7 @@ but found 2
at Assert.That(2).IsEqualTo(3).And.IsEqualTo(4)
""");
- await TUnitAssert.That(exception3.Message).IsEqualTo("""
+ await TUnitAssert.That(exception3.Message.NormalizeLineEndings()).IsEqualTo("""
Expected to be 4
or to be 5
but found 3
@@ -111,7 +111,7 @@ but found 3
at Assert.That(3).IsEqualTo(4).Or.IsEqualTo(5)
""");
- await TUnitAssert.That(exception4.Message).IsEqualTo("""
+ await TUnitAssert.That(exception4.Message.NormalizeLineEndings()).IsEqualTo("""
Expected to be 5
and to be 6
but found 4
@@ -119,7 +119,7 @@ but found 4
at Assert.That(4).IsEqualTo(5).And.IsEqualTo(6)
""");
- await TUnitAssert.That(exception5.Message).IsEqualTo("""
+ await TUnitAssert.That(exception5.Message.NormalizeLineEndings()).IsEqualTo("""
Expected to be 6
or to be 7
but found 5
@@ -171,49 +171,49 @@ public async Task Nested_Multiples()
var assertionException6 = (TUnitAssertionException) aggregateException.InnerExceptions[5];
var assertionException7 = (TUnitAssertionException) aggregateException.InnerExceptions[6];
- await TUnitAssert.That(assertionException1.Message).IsEqualTo("""
+ await TUnitAssert.That(assertionException1.Message.NormalizeLineEndings()).IsEqualTo("""
Expected to be 2
but found 1
at Assert.That(1).IsEqualTo(2)
""");
- await TUnitAssert.That(assertionException2.Message).IsEqualTo("""
+ await TUnitAssert.That(assertionException2.Message.NormalizeLineEndings()).IsEqualTo("""
Expected to be 3
but found 2
at Assert.That(2).IsEqualTo(3)
""");
- await TUnitAssert.That(assertionException3.Message).IsEqualTo("""
+ await TUnitAssert.That(assertionException3.Message.NormalizeLineEndings()).IsEqualTo("""
Expected to be 4
but found 3
at Assert.That(3).IsEqualTo(4)
""");
- await TUnitAssert.That(assertionException4.Message).IsEqualTo("""
+ await TUnitAssert.That(assertionException4.Message.NormalizeLineEndings()).IsEqualTo("""
Expected to be 5
but found 4
at Assert.That(4).IsEqualTo(5)
""");
- await TUnitAssert.That(assertionException5.Message).IsEqualTo("""
+ await TUnitAssert.That(assertionException5.Message.NormalizeLineEndings()).IsEqualTo("""
Expected to be 6
but found 5
at Assert.That(5).IsEqualTo(6)
""");
- await TUnitAssert.That(assertionException6.Message).IsEqualTo("""
+ await TUnitAssert.That(assertionException6.Message.NormalizeLineEndings()).IsEqualTo("""
Expected to be 7
but found 6
at Assert.That(6).IsEqualTo(7)
""");
- await TUnitAssert.That(assertionException7.Message).IsEqualTo("""
+ await TUnitAssert.That(assertionException7.Message.NormalizeLineEndings()).IsEqualTo("""
Expected to be 8
but found 7
diff --git a/TUnit.Assertions.Tests/Old/EquivalentAssertionTests.cs b/TUnit.Assertions.Tests/Old/EquivalentAssertionTests.cs
index ea82d335f6..841660fe8d 100644
--- a/TUnit.Assertions.Tests/Old/EquivalentAssertionTests.cs
+++ b/TUnit.Assertions.Tests/Old/EquivalentAssertionTests.cs
@@ -130,7 +130,7 @@ public async Task Different_Enumerables__Thrown_When_Non_Matching_Order()
var exception = await TUnitAssert.ThrowsAsync(async () => await TUnitAssert.That(array).IsEquivalentTo(list, CollectionOrdering.Matching));
- await TUnitAssert.That(exception!.Message).IsEqualTo(
+ await TUnitAssert.That(exception!.Message.NormalizeLineEndings()).IsEqualTo(
"""
Expected to be equivalent to [1, 2, 3, 4, 5]
but collection item at index 1 does not match: expected 2, but was 5
@@ -149,7 +149,7 @@ public async Task Different_Enumerables__Thrown_When_Non_Matching_Order2()
var exception = await TUnitAssert.ThrowsAsync(async () => await TUnitAssert.That(array).IsEquivalentTo(list, CollectionOrdering.Matching));
- await TUnitAssert.That(exception!.Message).IsEqualTo(
+ await TUnitAssert.That(exception!.Message.NormalizeLineEndings()).IsEqualTo(
"""
Expected to be equivalent to [1, 2, 3, 4, 5]
but collection item at index 1 does not match: expected 2, but was 5
@@ -177,7 +177,7 @@ public async Task Different_Mismatched_Objects_Still_Are_Not_Equivalent()
$"Received: \"Foo\"{Environment.NewLine}" +
$"{Environment.NewLine}" +
$"at Assert.That(result1).IsEquivalentTo(result2)";
- await TUnitAssert.That(exception!.Message).IsEqualTo(expectedMessage);
+ await TUnitAssert.That(exception!.Message.NormalizeLineEndings().NormalizeLineEndings()).IsEqualTo(expectedMessage.NormalizeLineEndings());
}
[Test]
@@ -197,7 +197,7 @@ public async Task Mismatched_Objects_Are_Not_Equivalent()
$"Received: null{Environment.NewLine}" +
$"{Environment.NewLine}" +
$"at Assert.That(object1).IsEquivalentTo(object2)";
- await TUnitAssert.That(exception!.Message).IsEqualTo(expectedMessage);
+ await TUnitAssert.That(exception!.Message.NormalizeLineEndings().NormalizeLineEndings()).IsEqualTo(expectedMessage.NormalizeLineEndings());
}
[Test]
@@ -233,7 +233,7 @@ public async Task Objects_With_Nested_Mismatch_Are_Not_Equivalent()
$"Received: null{Environment.NewLine}" +
$"{Environment.NewLine}" +
$"at Assert.That(object1).IsEquivalentTo(object2)";
- await TUnitAssert.That(exception!.Message).IsEqualTo(expectedMessage);
+ await TUnitAssert.That(exception!.Message.NormalizeLineEndings().NormalizeLineEndings()).IsEqualTo(expectedMessage.NormalizeLineEndings());
}
[Test]
@@ -365,7 +365,7 @@ public async Task Objects_With_Nested_Enumerable_Mismatch_Are_Not_Equivalent()
$"Received: null{Environment.NewLine}" +
$"{Environment.NewLine}" +
$"at Assert.That(object1).IsEquivalentTo(object2)";
- await TUnitAssert.That(exception!.Message).IsEqualTo(expectedMessage);
+ await TUnitAssert.That(exception!.Message.NormalizeLineEndings().NormalizeLineEndings()).IsEqualTo(expectedMessage.NormalizeLineEndings());
}
[Test]
@@ -496,7 +496,7 @@ public async Task Objects_With_Partial_Properties_Match_With_Full_Equivalency_Ar
$"Received: InnerClass{Environment.NewLine}" +
$"{Environment.NewLine}" +
$"at Assert.That(object1).IsEquivalentTo(object2)";
- await TUnitAssert.That(exception!.Message).IsEqualTo(expectedMessage);
+ await TUnitAssert.That(exception!.Message.NormalizeLineEndings().NormalizeLineEndings()).IsEqualTo(expectedMessage.NormalizeLineEndings());
}
[Test]
@@ -572,7 +572,7 @@ public async Task Objects_With_Mismatch_With_Partial_Equivalency_Kind_Are_Not_Eq
$"Received: \"Bar\"{Environment.NewLine}" +
$"{Environment.NewLine}" +
$"at Assert.That(object1).IsEquivalentTo(object2).WithPartialEquivalency()";
- await TUnitAssert.That(exception!.Message).IsEqualTo(expectedMessage);
+ await TUnitAssert.That(exception!.Message.NormalizeLineEndings().NormalizeLineEndings()).IsEqualTo(expectedMessage.NormalizeLineEndings());
}
[Test]
@@ -624,7 +624,7 @@ public async Task Object_With_Partial_Fields_Match_With_Full_Equivalency_Are_Not
$"Received: Int32{Environment.NewLine}" +
$"{Environment.NewLine}" +
$"at Assert.That(object1).IsEquivalentTo(object2)";
- await TUnitAssert.That(exception!.Message).IsEqualTo(expectedMessage);
+ await TUnitAssert.That(exception!.Message.NormalizeLineEndings().NormalizeLineEndings()).IsEqualTo(expectedMessage.NormalizeLineEndings());
}
[Test]
diff --git a/TUnit.Assertions.Tests/Old/StringEqualsAssertionTests.cs b/TUnit.Assertions.Tests/Old/StringEqualsAssertionTests.cs
index 874606d7bf..7768d5aab7 100644
--- a/TUnit.Assertions.Tests/Old/StringEqualsAssertionTests.cs
+++ b/TUnit.Assertions.Tests/Old/StringEqualsAssertionTests.cs
@@ -160,6 +160,6 @@ Volutpat vero est ea clita clita magna dolor nulla ipsum aliquyam nonumy.
$" ↑{Environment.NewLine}" +
$"{Environment.NewLine}" +
$"at Assert.That(value1).IsEqualTo(value2)";
- await TUnitAssert.That(exception!.Message).IsEqualTo(expectedMessage);
+ await TUnitAssert.That(exception!.Message.NormalizeLineEndings().NormalizeLineEndings()).IsEqualTo(expectedMessage.NormalizeLineEndings());
}
}
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 bb6caf4a1a..6d98f6861c 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
@@ -3568,12 +3568,6 @@ namespace .Extensions
public static . IsEqualTo(this . source, string? expected, [.("expected")] string? expectedExpression = null) { }
[.(2)]
public static . IsEqualTo(this . source, string? expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { }
- [.(3)]
- public static . IsEqualTo(this . source, string? expected, [.("expected")] string? expectedExpression = null)
- where TNonNullable : class { }
- [.(3)]
- public static . IsEqualTo(this . source, string? expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null)
- where TNonNullable : class { }
}
public static class StringIsEmptyAssertionExtensions
{
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 d8018896c4..5cc3222ee9 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
@@ -3548,10 +3548,6 @@ namespace .Extensions
{
public static . IsEqualTo(this . source, string? expected, [.("expected")] string? expectedExpression = null) { }
public static . IsEqualTo(this . source, string? expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { }
- public static . IsEqualTo(this . source, string? expected, [.("expected")] string? expectedExpression = null)
- where TNonNullable : class { }
- public static . IsEqualTo(this . source, string? expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null)
- where TNonNullable : class { }
}
public static class StringIsEmptyAssertionExtensions
{
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 cac2d5444c..ca9170a7aa 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
@@ -3568,12 +3568,6 @@ namespace .Extensions
public static . IsEqualTo(this . source, string? expected, [.("expected")] string? expectedExpression = null) { }
[.(2)]
public static . IsEqualTo(this . source, string? expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { }
- [.(3)]
- public static . IsEqualTo(this . source, string? expected, [.("expected")] string? expectedExpression = null)
- where TNonNullable : class { }
- [.(3)]
- public static . IsEqualTo(this . source, string? expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null)
- where TNonNullable : class { }
}
public static class StringIsEmptyAssertionExtensions
{
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 aa789906e2..47c892ea72 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
@@ -3118,10 +3118,6 @@ namespace .Extensions
{
public static . IsEqualTo(this . source, string? expected, [.("expected")] string? expectedExpression = null) { }
public static . IsEqualTo(this . source, string? expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { }
- public static . IsEqualTo(this . source, string? expected, [.("expected")] string? expectedExpression = null)
- where TNonNullable : class { }
- public static . IsEqualTo(this . source, string? expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null)
- where TNonNullable : class { }
}
public static class StringIsEmptyAssertionExtensions
{
From 6604d048b0266635d12601c985aa2bac01cbc7b2 Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Sun, 2 Nov 2025 01:05:20 +0000
Subject: [PATCH 5/6] feat: refactor StringEqualsAssertion to support generic
types for nullable and non-nullable strings
---
.../Conditions/StringEqualsAssertion.cs | 24 ++++++++---------
...Has_No_API_Changes.DotNet10_0.verified.txt | 26 +++++++++----------
..._Has_No_API_Changes.DotNet8_0.verified.txt | 24 ++++++++---------
..._Has_No_API_Changes.DotNet9_0.verified.txt | 26 +++++++++----------
...ary_Has_No_API_Changes.Net4_7.verified.txt | 24 ++++++++---------
5 files changed, 60 insertions(+), 64 deletions(-)
diff --git a/TUnit.Assertions/Conditions/StringEqualsAssertion.cs b/TUnit.Assertions/Conditions/StringEqualsAssertion.cs
index ab0a6ecb08..15b267ca0d 100644
--- a/TUnit.Assertions/Conditions/StringEqualsAssertion.cs
+++ b/TUnit.Assertions/Conditions/StringEqualsAssertion.cs
@@ -6,10 +6,10 @@ namespace TUnit.Assertions.Conditions;
///
/// Asserts that a string is equal to an expected value.
-/// Demonstrates multiple custom methods WITHOUT wrappers!
+/// Generic to support both nullable and non-nullable string sources.
///
-[AssertionExtension("IsEqualTo", OverloadResolutionPriority = 2)]
-public class StringEqualsAssertion : Assertion
+[AssertionExtension("IsEqualTo")]
+public class StringEqualsAssertion : Assertion
{
private readonly string? _expected;
private StringComparison _comparison = StringComparison.Ordinal;
@@ -18,7 +18,7 @@ public class StringEqualsAssertion : Assertion
private bool _ignoringWhitespace;
public StringEqualsAssertion(
- AssertionContext context,
+ AssertionContext context,
string? expected)
: base(context)
{
@@ -26,7 +26,7 @@ public StringEqualsAssertion(
}
public StringEqualsAssertion(
- AssertionContext context,
+ AssertionContext context,
string? expected,
StringComparison comparison)
: base(context)
@@ -38,7 +38,7 @@ public StringEqualsAssertion(
///
/// Makes the comparison case-insensitive.
///
- public StringEqualsAssertion IgnoringCase()
+ public StringEqualsAssertion IgnoringCase()
{
_comparison = StringComparison.OrdinalIgnoreCase;
Context.ExpressionBuilder.Append(".IgnoringCase()");
@@ -48,7 +48,7 @@ public StringEqualsAssertion IgnoringCase()
///
/// Specifies a custom string comparison type.
///
- public StringEqualsAssertion WithComparison(StringComparison comparison)
+ public StringEqualsAssertion WithComparison(StringComparison comparison)
{
_comparison = comparison;
Context.ExpressionBuilder.Append($".WithComparison({comparison})");
@@ -58,7 +58,7 @@ public StringEqualsAssertion WithComparison(StringComparison comparison)
///
/// Trims both strings before comparing.
///
- public StringEqualsAssertion WithTrimming()
+ public StringEqualsAssertion WithTrimming()
{
_trimming = true;
Context.ExpressionBuilder.Append(".WithTrimming()");
@@ -68,7 +68,7 @@ public StringEqualsAssertion WithTrimming()
///
/// Treats null and empty string as equal.
///
- public StringEqualsAssertion WithNullAndEmptyEquality()
+ public StringEqualsAssertion WithNullAndEmptyEquality()
{
_nullAndEmptyEquality = true;
Context.ExpressionBuilder.Append(".WithNullAndEmptyEquality()");
@@ -78,16 +78,16 @@ public StringEqualsAssertion WithNullAndEmptyEquality()
///
/// Removes all whitespace from both strings before comparing.
///
- public StringEqualsAssertion IgnoringWhitespace()
+ public StringEqualsAssertion IgnoringWhitespace()
{
_ignoringWhitespace = true;
Context.ExpressionBuilder.Append(".IgnoringWhitespace()");
return this;
}
- protected override Task CheckAsync(EvaluationMetadata metadata)
+ protected override Task CheckAsync(EvaluationMetadata metadata)
{
- var value = metadata.Value;
+ var value = metadata.Value as string;
var exception = metadata.Exception;
var actualValue = value;
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 6d98f6861c..498285588e 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
@@ -1070,18 +1070,18 @@ namespace .Conditions
public . IgnoringCase() { }
public . WithComparison( comparison) { }
}
- [.("IsEqualTo", OverloadResolutionPriority=2)]
- public class StringEqualsAssertion : .
+ [.("IsEqualTo")]
+ public class StringEqualsAssertion : .
{
- public StringEqualsAssertion(. context, string? expected) { }
- public StringEqualsAssertion(. context, string? expected, comparison) { }
- protected override .<.> CheckAsync(. metadata) { }
+ public StringEqualsAssertion(. context, string? expected) { }
+ public StringEqualsAssertion(. context, string? expected, comparison) { }
+ protected override .<.> CheckAsync(. metadata) { }
protected override string GetExpectation() { }
- public . IgnoringCase() { }
- public . IgnoringWhitespace() { }
- public . WithComparison( comparison) { }
- public . WithNullAndEmptyEquality() { }
- public . WithTrimming() { }
+ public . IgnoringCase() { }
+ public . IgnoringWhitespace() { }
+ public . WithComparison( comparison) { }
+ public . WithNullAndEmptyEquality() { }
+ public . WithTrimming() { }
}
[.("IsEmpty")]
public class StringIsEmptyAssertion : .
@@ -3564,10 +3564,8 @@ namespace .Extensions
}
public static class StringEqualsAssertionExtensions
{
- [.(2)]
- public static . IsEqualTo(this . source, string? expected, [.("expected")] string? expectedExpression = null) { }
- [.(2)]
- public static . IsEqualTo(this . source, string? expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { }
+ public static . IsEqualTo(this . source, string? expected, [.("expected")] string? expectedExpression = null) { }
+ public static . IsEqualTo(this . source, string? expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { }
}
public static class StringIsEmptyAssertionExtensions
{
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 5cc3222ee9..148b8936f9 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
@@ -1067,18 +1067,18 @@ namespace .Conditions
public . IgnoringCase() { }
public . WithComparison( comparison) { }
}
- [.("IsEqualTo", OverloadResolutionPriority=2)]
- public class StringEqualsAssertion : .
+ [.("IsEqualTo")]
+ public class StringEqualsAssertion : .
{
- public StringEqualsAssertion(. context, string? expected) { }
- public StringEqualsAssertion(. context, string? expected, comparison) { }
- protected override .<.> CheckAsync(. metadata) { }
+ public StringEqualsAssertion(. context, string? expected) { }
+ public StringEqualsAssertion(. context, string? expected, comparison) { }
+ protected override .<.> CheckAsync(. metadata) { }
protected override string GetExpectation() { }
- public . IgnoringCase() { }
- public . IgnoringWhitespace() { }
- public . WithComparison( comparison) { }
- public . WithNullAndEmptyEquality() { }
- public . WithTrimming() { }
+ public . IgnoringCase() { }
+ public . IgnoringWhitespace() { }
+ public . WithComparison( comparison) { }
+ public . WithNullAndEmptyEquality() { }
+ public . WithTrimming() { }
}
[.("IsEmpty")]
public class StringIsEmptyAssertion : .
@@ -3546,8 +3546,8 @@ namespace .Extensions
}
public static class StringEqualsAssertionExtensions
{
- public static . IsEqualTo(this . source, string? expected, [.("expected")] string? expectedExpression = null) { }
- public static . IsEqualTo(this . source, string? expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { }
+ public static . IsEqualTo(this . source, string? expected, [.("expected")] string? expectedExpression = null) { }
+ public static . IsEqualTo(this . source, string? expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { }
}
public static class StringIsEmptyAssertionExtensions
{
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 ca9170a7aa..1ef9b7b9b8 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
@@ -1070,18 +1070,18 @@ namespace .Conditions
public . IgnoringCase() { }
public . WithComparison( comparison) { }
}
- [.("IsEqualTo", OverloadResolutionPriority=2)]
- public class StringEqualsAssertion : .
+ [.("IsEqualTo")]
+ public class StringEqualsAssertion : .
{
- public StringEqualsAssertion(. context, string? expected) { }
- public StringEqualsAssertion(. context, string? expected, comparison) { }
- protected override .<.> CheckAsync(. metadata) { }
+ public StringEqualsAssertion(. context, string? expected) { }
+ public StringEqualsAssertion(. context, string? expected, comparison) { }
+ protected override .<.> CheckAsync(. metadata) { }
protected override string GetExpectation() { }
- public . IgnoringCase() { }
- public . IgnoringWhitespace() { }
- public . WithComparison( comparison) { }
- public . WithNullAndEmptyEquality() { }
- public . WithTrimming() { }
+ public . IgnoringCase() { }
+ public . IgnoringWhitespace() { }
+ public . WithComparison( comparison) { }
+ public . WithNullAndEmptyEquality() { }
+ public . WithTrimming() { }
}
[.("IsEmpty")]
public class StringIsEmptyAssertion : .
@@ -3564,10 +3564,8 @@ namespace .Extensions
}
public static class StringEqualsAssertionExtensions
{
- [.(2)]
- public static . IsEqualTo(this . source, string? expected, [.("expected")] string? expectedExpression = null) { }
- [.(2)]
- public static . IsEqualTo(this . source, string? expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { }
+ public static . IsEqualTo(this . source, string? expected, [.("expected")] string? expectedExpression = null) { }
+ public static . IsEqualTo(this . source, string? expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { }
}
public static class StringIsEmptyAssertionExtensions
{
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 47c892ea72..af57de0948 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
@@ -997,18 +997,18 @@ namespace .Conditions
public . IgnoringCase() { }
public . WithComparison( comparison) { }
}
- [.("IsEqualTo", OverloadResolutionPriority=2)]
- public class StringEqualsAssertion : .
+ [.("IsEqualTo")]
+ public class StringEqualsAssertion : .
{
- public StringEqualsAssertion(. context, string? expected) { }
- public StringEqualsAssertion(. context, string? expected, comparison) { }
- protected override .<.> CheckAsync(. metadata) { }
+ public StringEqualsAssertion(. context, string? expected) { }
+ public StringEqualsAssertion(. context, string? expected, comparison) { }
+ protected override .<.> CheckAsync(. metadata) { }
protected override string GetExpectation() { }
- public . IgnoringCase() { }
- public . IgnoringWhitespace() { }
- public . WithComparison( comparison) { }
- public . WithNullAndEmptyEquality() { }
- public . WithTrimming() { }
+ public . IgnoringCase() { }
+ public . IgnoringWhitespace() { }
+ public . WithComparison( comparison) { }
+ public . WithNullAndEmptyEquality() { }
+ public . WithTrimming() { }
}
[.("IsEmpty")]
public class StringIsEmptyAssertion : .
@@ -3116,8 +3116,8 @@ namespace .Extensions
}
public static class StringEqualsAssertionExtensions
{
- public static . IsEqualTo(this . source, string? expected, [.("expected")] string? expectedExpression = null) { }
- public static . IsEqualTo(this . source, string? expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { }
+ public static . IsEqualTo(this . source, string? expected, [.("expected")] string? expectedExpression = null) { }
+ public static . IsEqualTo(this . source, string? expected, comparison, [.("expected")] string? expectedExpression = null, [.("comparison")] string? comparisonExpression = null) { }
}
public static class StringIsEmptyAssertionExtensions
{
From 4d735e2c5595b36332990a6fd521b15c146fed45 Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Sun, 2 Nov 2025 01:41:35 +0000
Subject: [PATCH 6/6] feat: enhance AsyncMappedSatisfiesAssertion to support
custom assertion types
---
TUnit.Assertions/Conditions/MappedSatisfiesAssertion.cs | 7 ++++---
TUnit.Assertions/Extensions/AssertionExtensions.cs | 7 ++++---
...ons_Library_Has_No_API_Changes.DotNet10_0.verified.txt | 8 +++++---
...ions_Library_Has_No_API_Changes.DotNet8_0.verified.txt | 8 +++++---
...ions_Library_Has_No_API_Changes.DotNet9_0.verified.txt | 8 +++++---
...ertions_Library_Has_No_API_Changes.Net4_7.verified.txt | 8 +++++---
6 files changed, 28 insertions(+), 18 deletions(-)
diff --git a/TUnit.Assertions/Conditions/MappedSatisfiesAssertion.cs b/TUnit.Assertions/Conditions/MappedSatisfiesAssertion.cs
index cf254143a4..518cb67ae6 100644
--- a/TUnit.Assertions/Conditions/MappedSatisfiesAssertion.cs
+++ b/TUnit.Assertions/Conditions/MappedSatisfiesAssertion.cs
@@ -75,16 +75,17 @@ protected override async Task CheckAsync(EvaluationMetadata m.GetNameAsync(), assert => assert.IsEqualTo("John"));
///
-public class AsyncMappedSatisfiesAssertion : Assertion
+public class AsyncMappedSatisfiesAssertion : Assertion
+ where TAssertion : Assertion
{
private readonly Func> _selector;
- private readonly Func, Assertion?> _assertions;
+ private readonly Func