From a6ae1cfb6a296c91fc3aed2a9b8da0d037e31ba4 Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Tue, 14 Oct 2025 09:46:01 +0100
Subject: [PATCH 1/5] feat: add support for cross-type equality assertions with
IEquatable for structs
---
TUnit.Assertions.Tests/EquatableTests.cs | 268 +++++++++++++
.../Conditions/EquatableAssertion.cs | 92 +++++
.../Extensions/AssertionExtensions.cs | 372 ++----------------
3 files changed, 391 insertions(+), 341 deletions(-)
create mode 100644 TUnit.Assertions.Tests/EquatableTests.cs
create mode 100644 TUnit.Assertions/Conditions/EquatableAssertion.cs
diff --git a/TUnit.Assertions.Tests/EquatableTests.cs b/TUnit.Assertions.Tests/EquatableTests.cs
new file mode 100644
index 0000000000..fc47129ad3
--- /dev/null
+++ b/TUnit.Assertions.Tests/EquatableTests.cs
@@ -0,0 +1,268 @@
+#if NET6_0_OR_GREATER
+namespace TUnit.Assertions.Tests;
+
+///
+/// Tests for IEquatable<T> assertions where the actual and expected types differ.
+/// These tests verify that types implementing IEquatable with cross-type equality work correctly.
+/// Note: Only available on .NET 6+ due to overload resolution limitations in older frameworks.
+///
+public class EquatableTests
+{
+ // Example struct from GitHub issue #2972
+ public struct Wrapper : IEquatable, IEquatable
+ {
+ public long Value { get; set; }
+
+ public bool Equals(Wrapper other) => Value == other.Value;
+
+ public bool Equals(long other) => Value == other;
+
+ public override bool Equals(object? obj)
+ {
+ return obj switch
+ {
+ Wrapper wrapper => Equals(wrapper),
+ long l => Equals(l),
+ _ => false
+ };
+ }
+
+ public override int GetHashCode() => Value.GetHashCode();
+ }
+
+ // Struct with multiple IEquatable implementations
+ public struct MultiEquatable : IEquatable, IEquatable, IEquatable
+ {
+ public int IntValue { get; set; }
+ public string StringValue { get; set; }
+
+ public bool Equals(MultiEquatable other) =>
+ IntValue == other.IntValue && StringValue == other.StringValue;
+
+ public bool Equals(int other) => IntValue == other;
+
+ public bool Equals(string? other) => StringValue == other;
+
+ public override bool Equals(object? obj)
+ {
+ return obj switch
+ {
+ MultiEquatable m => Equals(m),
+ int i => Equals(i),
+ string s => Equals(s),
+ _ => false
+ };
+ }
+
+ public override int GetHashCode() => HashCode.Combine(IntValue, StringValue);
+ }
+
+ // Simple struct with IEquatable
+ public struct IntWrapper : IEquatable
+ {
+ public int Value { get; set; }
+
+ public bool Equals(int other) => Value == other;
+
+ public override bool Equals(object? obj) => obj is int i && Equals(i);
+
+ public override int GetHashCode() => Value.GetHashCode();
+ }
+
+ [Test]
+ public async Task Wrapper_IsEqualTo_Long_Success()
+ {
+ // Arrange
+ Wrapper wrapper = new() { Value = 42 };
+
+ // Act & Assert
+ await Assert.That(wrapper).IsEqualTo(42L);
+ }
+
+ [Test]
+ public async Task Wrapper_IsEqualTo_Long_Failure()
+ {
+ // Arrange
+ Wrapper wrapper = new() { Value = 42 };
+
+ // Act & Assert
+ await Assert.ThrowsAsync(async () =>
+ {
+ await Assert.That(wrapper).IsEqualTo(99L);
+ });
+ }
+
+ [Test]
+ public async Task Wrapper_IsEqualTo_Wrapper_Success()
+ {
+ // Arrange
+ Wrapper wrapper1 = new() { Value = 42 };
+ Wrapper wrapper2 = new() { Value = 42 };
+
+ // Act & Assert
+ await Assert.That(wrapper1).IsEqualTo(wrapper2);
+ }
+
+ [Test]
+ public async Task NullableWrapper_IsEqualTo_Long_Success()
+ {
+ // Arrange
+ Wrapper? wrapper = new Wrapper { Value = 42 };
+
+ // Act & Assert
+ await Assert.That(wrapper).IsEqualTo(42L);
+ }
+
+ [Test]
+ public async Task NullableWrapper_IsEqualTo_Long_NullFailure()
+ {
+ // Arrange
+ Wrapper? wrapper = null;
+
+ // Act & Assert
+ await Assert.ThrowsAsync(async () =>
+ {
+ await Assert.That(wrapper).IsEqualTo(42L);
+ });
+ }
+
+ [Test]
+ public async Task MultiEquatable_IsEqualTo_Int_Success()
+ {
+ // Arrange
+ MultiEquatable value = new() { IntValue = 100, StringValue = "test" };
+
+ // Act & Assert
+ await Assert.That(value).IsEqualTo(100);
+ }
+
+ [Test]
+ public async Task MultiEquatable_IsEqualTo_String_Success()
+ {
+ // Arrange
+ MultiEquatable value = new() { IntValue = 100, StringValue = "test" };
+
+ // Act & Assert
+ await Assert.That(value).IsEqualTo("test");
+ }
+
+ [Test]
+ public async Task IntWrapper_IsEqualTo_Int_Success()
+ {
+ // Arrange
+ IntWrapper wrapper = new() { Value = 123 };
+
+ // Act & Assert
+ await Assert.That(wrapper).IsEqualTo(123);
+ }
+
+ [Test]
+ public async Task IntWrapper_IsEqualTo_Int_Failure()
+ {
+ // Arrange
+ IntWrapper wrapper = new() { Value = 123 };
+
+ // Act & Assert
+ await Assert.ThrowsAsync(async () =>
+ {
+ await Assert.That(wrapper).IsEqualTo(456);
+ });
+ }
+
+ [Test]
+ public async Task EquatableAssertion_WithAnd_Success()
+ {
+ // Arrange
+ Wrapper wrapper = new() { Value = 42 };
+
+ // Act & Assert
+ await Assert.That(wrapper).IsEqualTo(42L).And.IsEqualTo(new Wrapper { Value = 42 });
+ }
+
+ [Test]
+ public async Task EquatableAssertion_WithOr_Success()
+ {
+ // Arrange
+ Wrapper wrapper = new() { Value = 42 };
+
+ // Act & Assert - should pass because first condition is true
+ await Assert.That(wrapper).IsEqualTo(42L).Or.IsEqualTo(new Wrapper { Value = 99 });
+ }
+
+ [Test]
+ public async Task EquatableAssertion_WithAnd_Failure()
+ {
+ // Arrange
+ Wrapper wrapper = new() { Value = 42 };
+
+ // Act & Assert
+ await Assert.ThrowsAsync(async () =>
+ {
+ await Assert.That(wrapper).IsEqualTo(42L).And.IsEqualTo(new Wrapper { Value = 99 });
+ });
+ }
+
+ [Test]
+ public async Task EquatableAssertion_ChainedWithAndContinuation()
+ {
+ // Arrange
+ Wrapper wrapper = new() { Value = 42 };
+
+ // Act & Assert - test that And continuation works
+ await Assert.That(wrapper).IsEqualTo(42L).And.IsEqualTo(42L);
+ }
+
+ [Test]
+ public async Task EquatableAssertion_ChainedWithOrContinuation()
+ {
+ // Arrange
+ Wrapper wrapper = new() { Value = 42 };
+
+ // Act & Assert - test that Or continuation works
+ await Assert.That(wrapper).IsEqualTo(99L).Or.IsEqualTo(42L);
+ }
+
+ [Test]
+ public async Task NullableIntWrapper_IsEqualTo_Int_Success()
+ {
+ // Arrange
+ IntWrapper? wrapper = new IntWrapper { Value = 789 };
+
+ // Act & Assert
+ await Assert.That(wrapper).IsEqualTo(789);
+ }
+
+ [Test]
+ public async Task NullableIntWrapper_IsEqualTo_Int_NullFailure()
+ {
+ // Arrange
+ IntWrapper? wrapper = null;
+
+ // Act & Assert
+ await Assert.ThrowsAsync(async () =>
+ {
+ await Assert.That(wrapper).IsEqualTo(789);
+ });
+ }
+
+ // Edge case: Ensure the constraint-based overload doesn't interfere with standard equality
+ [Test]
+ public async Task StandardEquality_StillWorks_ForSameTypes()
+ {
+ // Arrange
+ int value = 42;
+
+ // Act & Assert - should use standard IsEqualTo, not the IEquatable overload
+ await Assert.That(value).IsEqualTo(42);
+ }
+
+ // Test to verify the example from GitHub issue #2972 works exactly as requested
+ [Test]
+ public async Task GitHubIssue2972_Example()
+ {
+ // This is the exact example from the GitHub issue
+ Wrapper value = new() { Value = 1 };
+ await Assert.That(value).IsEqualTo(1L);
+ }
+}
+#endif
diff --git a/TUnit.Assertions/Conditions/EquatableAssertion.cs b/TUnit.Assertions/Conditions/EquatableAssertion.cs
new file mode 100644
index 0000000000..ae1e8a65d8
--- /dev/null
+++ b/TUnit.Assertions/Conditions/EquatableAssertion.cs
@@ -0,0 +1,92 @@
+using TUnit.Assertions.Core;
+
+namespace TUnit.Assertions.Conditions;
+
+///
+/// Asserts that a value implementing IEquatable<TExpected> is equal to an expected value of a different type.
+/// This allows comparing types that implement IEquatable with cross-type equality.
+/// Example: A Wrapper struct implementing IEquatable<long> can be compared directly to a long value.
+///
+public class EquatableAssertion : Assertion
+ where TActual : IEquatable
+{
+ private readonly TExpected _expected;
+
+ public EquatableAssertion(
+ AssertionContext context,
+ TExpected expected)
+ : base(context)
+ {
+ _expected = expected;
+ }
+
+ 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().FullName}"));
+ }
+
+ if (value == null)
+ {
+ return Task.FromResult(AssertionResult.Failed("value was null"));
+ }
+
+ // Use IEquatable.Equals for comparison
+ if (value.Equals(_expected))
+ {
+ return Task.FromResult(AssertionResult.Passed);
+ }
+
+ return Task.FromResult(AssertionResult.Failed($"found {value}"));
+ }
+
+ protected override string GetExpectation() => $"to be equal to {_expected}";
+}
+
+///
+/// Asserts that a nullable value type implementing IEquatable<TExpected> is equal to an expected value.
+/// Handles nullable structs that implement IEquatable.
+///
+public class NullableEquatableAssertion : Assertion
+ where TActual : struct, IEquatable
+{
+ private readonly TExpected _expected;
+
+ public NullableEquatableAssertion(
+ AssertionContext context,
+ TExpected expected)
+ : base(context)
+ {
+ _expected = expected;
+ }
+
+ 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().FullName}"));
+ }
+
+ if (!value.HasValue)
+ {
+ return Task.FromResult(AssertionResult.Failed("value was null"));
+ }
+
+ // Use IEquatable.Equals for comparison
+ if (value.Value.Equals(_expected))
+ {
+ return Task.FromResult(AssertionResult.Passed);
+ }
+
+ return Task.FromResult(AssertionResult.Failed($"found {value.Value}"));
+ }
+
+ protected override string GetExpectation() => $"to be equal to {_expected}";
+}
diff --git a/TUnit.Assertions/Extensions/AssertionExtensions.cs b/TUnit.Assertions/Extensions/AssertionExtensions.cs
index 2789d9f53a..c03954cf95 100644
--- a/TUnit.Assertions/Extensions/AssertionExtensions.cs
+++ b/TUnit.Assertions/Extensions/AssertionExtensions.cs
@@ -157,6 +157,37 @@ public static DateTimeOffsetEqualsAssertion IsEqualTo(
return new DateTimeOffsetEqualsAssertion(source.Context, expected);
}
+ ///
+ /// Asserts that a struct implementing IEquatable<TExpected> is equal to the expected value.
+ /// This enables direct equality comparisons for structs with cross-type IEquatable implementations.
+ /// Example: A Wrapper struct implementing IEquatable<long> can be compared directly to a long value.
+ /// Note: In cases of ambiguity with same-type comparisons, explicitly specify the type parameter.
+ ///
+ public static EquatableAssertion IsEqualTo(
+ this IAssertionSource source,
+ TExpected expected,
+ [CallerArgumentExpression(nameof(expected))] string? expression = null)
+ where TActual : struct, IEquatable
+ {
+ source.Context.ExpressionBuilder.Append($".IsEqualTo({expression})");
+ return new EquatableAssertion(source.Context, expected);
+ }
+
+ ///
+ /// Asserts that a nullable struct implementing IEquatable<TExpected> is equal to the expected value.
+ /// Handles nullable structs with cross-type IEquatable implementations.
+ /// Note: In cases of ambiguity with same-type comparisons, explicitly specify the type parameter.
+ ///
+ public static NullableEquatableAssertion IsEqualTo(
+ this IAssertionSource source,
+ TExpected expected,
+ [CallerArgumentExpression(nameof(expected))] string? expression = null)
+ where TActual : struct, IEquatable
+ {
+ source.Context.ExpressionBuilder.Append($".IsEqualTo({expression})");
+ return new NullableEquatableAssertion(source.Context, expected);
+ }
+
// ============ COMPARISONS ============
// IsGreaterThan, IsGreaterThanOrEqualTo, IsLessThan, IsLessThanOrEqualTo, and IsBetween
// are now generated by AssertionExtensionGenerator
@@ -225,133 +256,6 @@ public static LessThanAssertion IsNegative(
// ============ BOOLEAN ============
// IsTrue and IsFalse are now generated by AssertionExtensionGenerator for IAssertionSource
- // Only And/Or continuation overloads remain here since generator doesn't create those
-
- ///
- /// Asserts that the boolean value is true (And continuation overload).
- ///
- public static Chaining.AndAssertion IsTrue(
- this AndContinuation source)
- {
- source.Context.ExpressionBuilder.Append(".IsTrue()");
- var newAssertion = new Bool_IsTrue_Assertion(source.Context);
- return new Chaining.AndAssertion(source.PreviousAssertion, newAssertion);
- }
-
- ///
- /// Asserts that the boolean value is true (Or continuation overload).
- ///
- public static Chaining.OrAssertion IsTrue(
- this OrContinuation source)
- {
- source.Context.ExpressionBuilder.Append(".IsTrue()");
- var newAssertion = new Bool_IsTrue_Assertion(source.Context);
- return new Chaining.OrAssertion(source.PreviousAssertion, newAssertion);
- }
-
- ///
- /// Asserts that the boolean value is false (And continuation overload).
- ///
- public static Chaining.AndAssertion IsFalse(
- this AndContinuation source)
- {
- source.Context.ExpressionBuilder.Append(".IsFalse()");
- var newAssertion = new Bool_IsFalse_Assertion(source.Context);
- return new Chaining.AndAssertion(source.PreviousAssertion, newAssertion);
- }
-
- ///
- /// Asserts that the boolean value is false (Or continuation overload).
- ///
- public static Chaining.OrAssertion IsFalse(
- this OrContinuation source)
- {
- source.Context.ExpressionBuilder.Append(".IsFalse()");
- var newAssertion = new Bool_IsFalse_Assertion(source.Context);
- return new Chaining.OrAssertion(source.PreviousAssertion, newAssertion);
- }
-
- // ============ AND/OR CONTINUATION OVERLOADS FOR COMMON METHODS ============
-
- ///
- /// Asserts that the value is equal to the expected value (And continuation overload).
- ///
- public static Chaining.AndAssertion IsEqualTo(
- this AndContinuation source,
- TValue expected,
- [CallerArgumentExpression(nameof(expected))] string? expression = null)
- {
- source.Context.ExpressionBuilder.Append($".IsEqualTo({expression})");
- var newAssertion = new EqualsAssertion(source.Context, expected);
- return new Chaining.AndAssertion(source.PreviousAssertion, newAssertion);
- }
-
- ///
- /// Asserts that the value is equal to the expected value (Or continuation overload).
- ///
- public static Chaining.OrAssertion IsEqualTo(
- this OrContinuation source,
- TValue expected,
- [CallerArgumentExpression(nameof(expected))] string? expression = null)
- {
- source.Context.ExpressionBuilder.Append($".IsEqualTo({expression})");
- var newAssertion = new EqualsAssertion(source.Context, expected);
- return new Chaining.OrAssertion(source.PreviousAssertion, newAssertion);
- }
-
- ///
- /// Asserts that the string is equal to the expected value using the specified comparison (And continuation overload).
- ///
- public static Chaining.AndAssertion IsEqualTo(
- this AndContinuation source,
- string expected,
- StringComparison comparison,
- [CallerArgumentExpression(nameof(expected))] string? expression = null)
- {
- source.Context.ExpressionBuilder.Append($".IsEqualTo({expression}, StringComparison.{comparison})");
- var newAssertion = new StringEqualsAssertion(source.Context, expected).WithComparison(comparison);
- return new Chaining.AndAssertion(source.PreviousAssertion, newAssertion);
- }
-
- ///
- /// Asserts that the string is equal to the expected value using the specified comparison (Or continuation overload).
- ///
- public static Chaining.OrAssertion IsEqualTo(
- this OrContinuation source,
- string expected,
- StringComparison comparison,
- [CallerArgumentExpression(nameof(expected))] string? expression = null)
- {
- source.Context.ExpressionBuilder.Append($".IsEqualTo({expression}, StringComparison.{comparison})");
- var newAssertion = new StringEqualsAssertion(source.Context, expected).WithComparison(comparison);
- return new Chaining.OrAssertion(source.PreviousAssertion, newAssertion);
- }
-
- ///
- /// Asserts that the value is not equal to the expected value (And continuation overload).
- ///
- public static Chaining.AndAssertion IsNotEqualTo(
- this AndContinuation source,
- TValue expected,
- [CallerArgumentExpression(nameof(expected))] string? expression = null)
- {
- source.Context.ExpressionBuilder.Append($".IsNotEqualTo({expression})");
- var newAssertion = new NotEqualsAssertion(source.Context, expected);
- return new Chaining.AndAssertion(source.PreviousAssertion, newAssertion);
- }
-
- ///
- /// Asserts that the value is not equal to the expected value (Or continuation overload).
- ///
- public static Chaining.OrAssertion IsNotEqualTo(
- this OrContinuation source,
- TValue expected,
- [CallerArgumentExpression(nameof(expected))] string? expression = null)
- {
- source.Context.ExpressionBuilder.Append($".IsNotEqualTo({expression})");
- var newAssertion = new NotEqualsAssertion(source.Context, expected);
- return new Chaining.OrAssertion(source.PreviousAssertion, newAssertion);
- }
// ============ TYPE CHECKS ============
// IsTypeOf(Type), IsAssignableTo, IsNotAssignableTo are now generated by AssertionExtensionGenerator
@@ -392,33 +296,6 @@ public static IsTypeOfRuntimeAssertion IsOfType(
return new IsTypeOfRuntimeAssertion(source.Context, expectedType);
}
- ///
- /// Asserts that the value is of the specified type (runtime Type parameter, for AndContinuation).
- /// Example: await Assert.That(obj).IsEqualTo("foo").And.IsOfType(typeof(string));
- ///
- public static Chaining.AndAssertion IsOfType(
- this AndContinuation source,
- Type expectedType)
- {
- source.Context.ExpressionBuilder.Append($".IsOfType(typeof({expectedType.Name}))");
- var newAssertion = new IsTypeOfRuntimeAssertion(source.Context, expectedType);
- return new Chaining.AndAssertion(source.PreviousAssertion, newAssertion);
- }
-
- ///
- /// Asserts that the value is of the specified type (runtime Type parameter, for OrContinuation).
- /// Example: await Assert.That(obj).IsEqualTo("foo").Or.IsOfType(typeof(string));
- ///
- public static Chaining.OrAssertion IsOfType(
- this OrContinuation source,
- Type expectedType)
- {
- source.Context.ExpressionBuilder.Append($".IsOfType(typeof({expectedType.Name}))");
- var newAssertion = new IsTypeOfRuntimeAssertion(source.Context, expectedType);
- return new Chaining.OrAssertion(source.PreviousAssertion, newAssertion);
- }
-
-
///
/// Asserts on a member of an object using a lambda selector.
/// Returns an assertion on the member value for further chaining.
@@ -462,17 +339,6 @@ public static StringLengthAssertion HasLength(
return new StringLengthAssertion(source.Context, expectedLength);
}
- ///
- /// Returns a wrapper for string length assertions (And continuation overload).
- /// Example: await Assert.That(str).IsNotNull().And.HasLength().EqualTo(5);
- ///
- public static LengthWrapper HasLength(
- this AndContinuation source)
- {
- source.Context.ExpressionBuilder.Append(".HasLength()");
- return new LengthWrapper(source.Context);
- }
-
// ============ DICTIONARY ASSERTIONS ============
///
@@ -639,35 +505,6 @@ public static CollectionContainsPredicateAssertion, TItem> Co
return new CollectionContainsPredicateAssertion, TItem>(source.Context, predicate);
}
- ///
- /// Asserts that the collection contains the expected item (And continuation overload).
- /// Returns AndAssertion to enable And chaining and prevent mixing with Or.
- ///
- public static Chaining.AndAssertion Contains(
- this AndContinuation source,
- TItem expected,
- [CallerArgumentExpression(nameof(expected))] string? expression = null)
- where TCollection : IEnumerable
- {
- source.Context.ExpressionBuilder.Append($".Contains({expression})");
- var newAssertion = new CollectionContainsAssertion(source.Context, expected);
- return new Chaining.AndAssertion(source.PreviousAssertion, newAssertion);
- }
-
- ///
- /// Asserts that the collection contains the expected item (Or continuation overload).
- /// Returns OrAssertion to enable Or chaining and prevent mixing with And.
- ///
- public static Chaining.OrAssertion Contains(
- this OrContinuation source,
- TItem expected,
- [CallerArgumentExpression(nameof(expected))] string? expression = null)
- where TCollection : IEnumerable
- {
- source.Context.ExpressionBuilder.Append($".Contains({expression})");
- var newAssertion = new CollectionContainsAssertion(source.Context, expected);
- return new Chaining.OrAssertion(source.PreviousAssertion, newAssertion);
- }
///
/// Asserts that the collection does NOT contain the expected item.
@@ -1220,55 +1057,6 @@ public static ExceptionMessageAssertion HasMessageContaining(
return new ExceptionMessageAssertion(mappedContext, expectedSubstring, comparison);
}
- ///
- /// Asserts that an exception's Message property contains the expected substring (And continuation overload).
- ///
- public static Chaining.AndAssertion HasMessageContaining(
- this AndContinuation source,
- string expectedSubstring)
- {
- source.Context.ExpressionBuilder.Append($".HasMessageContaining(\"{expectedSubstring}\")");
- var newAssertion = new HasMessageContainingAssertion(source.Context, expectedSubstring);
- return new Chaining.AndAssertion(source.PreviousAssertion, newAssertion);
- }
-
- ///
- /// Asserts that an exception's Message property contains the expected substring (Or continuation overload).
- ///
- public static Chaining.OrAssertion HasMessageContaining(
- this OrContinuation source,
- string expectedSubstring)
- {
- source.Context.ExpressionBuilder.Append($".HasMessageContaining(\"{expectedSubstring}\")");
- var newAssertion = new HasMessageContainingAssertion(source.Context, expectedSubstring);
- return new Chaining.OrAssertion(source.PreviousAssertion, newAssertion);
- }
-
- ///
- /// Asserts that an exception's Message property contains the expected substring using the specified comparison (And continuation overload).
- ///
- public static Chaining.AndAssertion HasMessageContaining(
- this AndContinuation source,
- string expectedSubstring,
- StringComparison comparison)
- {
- source.Context.ExpressionBuilder.Append($".HasMessageContaining(\"{expectedSubstring}\", StringComparison.{comparison})");
- var newAssertion = new HasMessageContainingAssertion(source.Context, expectedSubstring, comparison);
- return new Chaining.AndAssertion(source.PreviousAssertion, newAssertion);
- }
-
- ///
- /// Asserts that an exception's Message property contains the expected substring using the specified comparison (Or continuation overload).
- ///
- public static Chaining.OrAssertion HasMessageContaining(
- this OrContinuation source,
- string expectedSubstring,
- StringComparison comparison)
- {
- source.Context.ExpressionBuilder.Append($".HasMessageContaining(\"{expectedSubstring}\", StringComparison.{comparison})");
- var newAssertion = new HasMessageContainingAssertion(source.Context, expectedSubstring, comparison);
- return new Chaining.OrAssertion(source.PreviousAssertion, newAssertion);
- }
///
/// Asserts that the exception message contains the specified substring.
@@ -1355,55 +1143,6 @@ public static HasMessageEqualToAssertion HasMessageEqualTo(
return new HasMessageEqualToAssertion(source.Context, expectedMessage, comparison);
}
- ///
- /// Asserts that an exception's Message property exactly equals the expected string (And continuation overload).
- ///
- public static Chaining.AndAssertion HasMessageEqualTo(
- this AndContinuation source,
- string expectedMessage)
- {
- source.Context.ExpressionBuilder.Append($".HasMessageEqualTo(\"{expectedMessage}\")");
- var newAssertion = new HasMessageEqualToAssertion(source.Context, expectedMessage);
- return new Chaining.AndAssertion(source.PreviousAssertion, newAssertion);
- }
-
- ///
- /// Asserts that an exception's Message property exactly equals the expected string (Or continuation overload).
- ///
- public static Chaining.OrAssertion HasMessageEqualTo(
- this OrContinuation source,
- string expectedMessage)
- {
- source.Context.ExpressionBuilder.Append($".HasMessageEqualTo(\"{expectedMessage}\")");
- var newAssertion = new HasMessageEqualToAssertion(source.Context, expectedMessage);
- return new Chaining.OrAssertion(source.PreviousAssertion, newAssertion);
- }
-
- ///
- /// Asserts that an exception's Message property exactly equals the expected string using the specified string comparison (And continuation overload).
- ///
- public static Chaining.AndAssertion HasMessageEqualTo(
- this AndContinuation source,
- string expectedMessage,
- StringComparison comparison)
- {
- source.Context.ExpressionBuilder.Append($".HasMessageEqualTo(\"{expectedMessage}\", StringComparison.{comparison})");
- var newAssertion = new HasMessageEqualToAssertion(source.Context, expectedMessage, comparison);
- return new Chaining.AndAssertion(source.PreviousAssertion, newAssertion);
- }
-
- ///
- /// Asserts that an exception's Message property exactly equals the expected string using the specified string comparison (Or continuation overload).
- ///
- public static Chaining.OrAssertion HasMessageEqualTo(
- this OrContinuation source,
- string expectedMessage,
- StringComparison comparison)
- {
- source.Context.ExpressionBuilder.Append($".HasMessageEqualTo(\"{expectedMessage}\", StringComparison.{comparison})");
- var newAssertion = new HasMessageEqualToAssertion(source.Context, expectedMessage, comparison);
- return new Chaining.OrAssertion(source.PreviousAssertion, newAssertion);
- }
///
/// Asserts that an exception's Message property starts with the expected string.
@@ -1430,55 +1169,6 @@ public static HasMessageStartingWithAssertion HasMessageStartingWith(source.Context, expectedPrefix, comparison);
}
- ///
- /// Asserts that an exception's Message property starts with the expected string (And continuation overload).
- ///
- public static Chaining.AndAssertion HasMessageStartingWith(
- this AndContinuation source,
- string expectedPrefix)
- {
- source.Context.ExpressionBuilder.Append($".HasMessageStartingWith(\"{expectedPrefix}\")");
- var newAssertion = new HasMessageStartingWithAssertion(source.Context, expectedPrefix);
- return new Chaining.AndAssertion(source.PreviousAssertion, newAssertion);
- }
-
- ///
- /// Asserts that an exception's Message property starts with the expected string (Or continuation overload).
- ///
- public static Chaining.OrAssertion HasMessageStartingWith(
- this OrContinuation source,
- string expectedPrefix)
- {
- source.Context.ExpressionBuilder.Append($".HasMessageStartingWith(\"{expectedPrefix}\")");
- var newAssertion = new HasMessageStartingWithAssertion(source.Context, expectedPrefix);
- return new Chaining.OrAssertion(source.PreviousAssertion, newAssertion);
- }
-
- ///
- /// Asserts that an exception's Message property starts with the expected string using the specified string comparison (And continuation overload).
- ///
- public static Chaining.AndAssertion HasMessageStartingWith(
- this AndContinuation source,
- string expectedPrefix,
- StringComparison comparison)
- {
- source.Context.ExpressionBuilder.Append($".HasMessageStartingWith(\"{expectedPrefix}\", StringComparison.{comparison})");
- var newAssertion = new HasMessageStartingWithAssertion(source.Context, expectedPrefix, comparison);
- return new Chaining.AndAssertion(source.PreviousAssertion, newAssertion);
- }
-
- ///
- /// Asserts that an exception's Message property starts with the expected string using the specified string comparison (Or continuation overload).
- ///
- public static Chaining.OrAssertion HasMessageStartingWith(
- this OrContinuation source,
- string expectedPrefix,
- StringComparison comparison)
- {
- source.Context.ExpressionBuilder.Append($".HasMessageStartingWith(\"{expectedPrefix}\", StringComparison.{comparison})");
- var newAssertion = new HasMessageStartingWithAssertion(source.Context, expectedPrefix, comparison);
- return new Chaining.OrAssertion(source.PreviousAssertion, newAssertion);
- }
///
/// Asserts that the DateTime is after or equal to the expected DateTime.
From e70d591aefef44e5159cb7ae9f4c30ddb68c2d7c Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Tue, 14 Oct 2025 09:48:41 +0100
Subject: [PATCH 2/5] Add EquatableAssertion and NullableEquatableAssertion
classes to assertion library
- Introduced EquatableAssertion for asserting equality between two types.
- Added NullableEquatableAssertion to handle nullable types in assertions.
- Updated various extension methods to remove redundant overloads and improve clarity.
- Ensured consistency across .NET 4.7, .NET 8.0, and .NET 9.0 verified files.
---
...Has_No_API_Changes.DotNet10_0.verified.txt | 47 +++++++------------
..._Has_No_API_Changes.DotNet8_0.verified.txt | 47 +++++++------------
..._Has_No_API_Changes.DotNet9_0.verified.txt | 47 +++++++------------
...ary_Has_No_API_Changes.Net4_7.verified.txt | 47 +++++++------------
4 files changed, 72 insertions(+), 116 deletions(-)
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 1d47957be5..9199048d75 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
@@ -680,6 +680,13 @@ namespace .Conditions
public . IgnoringType() { }
public . Within(object tolerance) { }
}
+ public class EquatableAssertion : .
+ where TActual :
+ {
+ public EquatableAssertion(. context, TExpected expected) { }
+ protected override .<.> CheckAsync(. metadata) { }
+ protected override string GetExpectation() { }
+ }
public static class ExceptionAssertionExtensions
{
[.(ExpectationMessage="to have a help link")]
@@ -994,6 +1001,13 @@ namespace .Conditions
protected override .<.> CheckAsync(. metadata) { }
protected override string GetExpectation() { }
}
+ public class NullableEquatableAssertion : .
+ where TActual : struct,
+ {
+ public NullableEquatableAssertion(. context, TExpected expected) { }
+ protected override .<.> CheckAsync(. metadata) { }
+ protected override string GetExpectation() { }
+ }
[.<.Process>("EnableRaisingEvents", CustomName="DoesNotHaveEventRaisingEnabled", ExpectationMessage="have event raising enabled", NegateLogic=true)]
[.<.Process>("EnableRaisingEvents", ExpectationMessage="have event raising enabled")]
[.<.Process>("HasExited", CustomName="HasNotExited", ExpectationMessage="have exited", NegateLogic=true)]
@@ -1541,14 +1555,10 @@ namespace .Extensions
public static . CompletesWithin(this . source, timeout, [.("timeout")] string? expression = null) { }
public static .<., TItem> Contains(this .<.> source, predicate, [.("predicate")] string? expression = null) { }
public static .<., TItem> Contains(this .<.> source, TItem expected, [.("expected")] string? expression = null) { }
- public static . Contains(this . source, TItem expected, [.("expected")] string? expression = null)
- where TCollection : . { }
public static . Contains(this . source, predicate, [.("predicate")] string? expression = null)
where TCollection : . { }
public static . Contains(this . source, TItem expected, [.("expected")] string? expression = null)
where TCollection : . { }
- public static . Contains(this . source, TItem expected, [.("expected")] string? expression = null)
- where TCollection : . { }
public static . ContainsKey(this .<.> source, TKey key, [.("key")] string? expression = null) { }
public static . ContainsKey(this .<.> source, TKey key, . comparer, [.("key")] string? expression = null) { }
public static . ContainsKey(this . source, TKey key, [.("key")] string? expression = null)
@@ -1583,26 +1593,13 @@ namespace .Extensions
public static ..HasFlagAssertion HasFlag(this . source, TEnum expectedFlag, [.("expectedFlag")] string? expression = null)
where TEnum : struct, { }
public static ..LengthWrapper HasLength(this . source) { }
- public static ..LengthWrapper HasLength(this . source) { }
public static . HasLength(this . source, int expectedLength, [.("expectedLength")] string? expression = null) { }
public static . HasMember(this . source, .<> memberSelector) { }
- public static . HasMessageContaining(this . source, string expectedSubstring) { }
public static . HasMessageContaining(this . source, string expectedSubstring) { }
- public static . HasMessageContaining(this . source, string expectedSubstring) { }
- public static . HasMessageContaining(this . source, string expectedSubstring, comparison) { }
public static . HasMessageContaining(this . source, string expectedSubstring, comparison) { }
- public static . HasMessageContaining(this . source, string expectedSubstring, comparison) { }
- public static . HasMessageEqualTo