diff --git a/TUnit.Assertions.Tests/IgnoringTypeEquivalentTests.cs b/TUnit.Assertions.Tests/IgnoringTypeEquivalentTests.cs new file mode 100644 index 0000000000..782385da24 --- /dev/null +++ b/TUnit.Assertions.Tests/IgnoringTypeEquivalentTests.cs @@ -0,0 +1,230 @@ +namespace TUnit.Assertions.Tests; + +public class IgnoringTypeEquivalentTests +{ + [Test] + public async Task IgnoringType_Generic_DateTime_Properties_Are_Ignored() + { + var object1 = new MyClassWithDates + { + Name = "Test", + CreatedDate = new DateTime(2023, 1, 1), + ModifiedDate = new DateTime(2023, 1, 2), + Value = 123 + }; + + var object2 = new MyClassWithDates + { + Name = "Test", + CreatedDate = new DateTime(2024, 6, 15), + ModifiedDate = new DateTime(2024, 7, 20), + Value = 123 + }; + + await TUnitAssert.That(object1) + .IsEquivalentTo(object2) + .IgnoringType(); + } + + [Test] + public async Task IgnoringType_NonGeneric_DateTime_Properties_Are_Ignored() + { + var object1 = new MyClassWithDates + { + Name = "Test", + CreatedDate = new DateTime(2023, 1, 1), + ModifiedDate = new DateTime(2023, 1, 2), + Value = 123 + }; + + var object2 = new MyClassWithDates + { + Name = "Test", + CreatedDate = new DateTime(2024, 6, 15), + ModifiedDate = new DateTime(2024, 7, 20), + Value = 123 + }; + + await TUnitAssert.That(object1) + .IsEquivalentTo(object2) + .IgnoringType(typeof(DateTime)); + } + + [Test] + public async Task IgnoringType_Nullable_DateTime_Properties_Are_Ignored() + { + var object1 = new MyClassWithNullableDates + { + Name = "Test", + OptionalDate = new DateTime(2023, 1, 1), + Value = 123 + }; + + var object2 = new MyClassWithNullableDates + { + Name = "Test", + OptionalDate = new DateTime(2024, 6, 15), + Value = 123 + }; + + await TUnitAssert.That(object1) + .IsEquivalentTo(object2) + .IgnoringType(); + } + + [Test] + public async Task IgnoringType_DateTimeOffset_Properties_Are_Ignored() + { + var object1 = new MyClassWithDateTimeOffset + { + Name = "Test", + Timestamp = new DateTimeOffset(2023, 1, 1, 0, 0, 0, TimeSpan.Zero), + Value = 123 + }; + + var object2 = new MyClassWithDateTimeOffset + { + Name = "Test", + Timestamp = new DateTimeOffset(2024, 6, 15, 12, 30, 45, TimeSpan.FromHours(2)), + Value = 123 + }; + + await TUnitAssert.That(object1) + .IsEquivalentTo(object2) + .IgnoringType(); + } + + [Test] + public async Task IgnoringType_Multiple_Types_Can_Be_Ignored() + { + var object1 = new MyClassWithMultipleTypes + { + Name = "Test", + CreatedDate = new DateTime(2023, 1, 1), + Guid = Guid.NewGuid(), + Value = 123 + }; + + var object2 = new MyClassWithMultipleTypes + { + Name = "Test", + CreatedDate = new DateTime(2024, 6, 15), + Guid = Guid.NewGuid(), + Value = 123 + }; + + await TUnitAssert.That(object1) + .IsEquivalentTo(object2) + .IgnoringType() + .IgnoringType(); + } + + [Test] + public async Task IgnoringType_Fields_Are_Also_Ignored() + { + var object1 = new MyClassWithDateFields + { + Name = "Test", + CreatedDateField = new DateTime(2023, 1, 1), + Value = 123 + }; + + var object2 = new MyClassWithDateFields + { + Name = "Test", + CreatedDateField = new DateTime(2024, 6, 15), + Value = 123 + }; + + await TUnitAssert.That(object1) + .IsEquivalentTo(object2) + .IgnoringType(); + } + + [Test] + public async Task NotEquivalentTo_IgnoringType_Works_Correctly() + { + var object1 = new MyClassWithDates + { + Name = "Different", + CreatedDate = new DateTime(2023, 1, 1), + ModifiedDate = new DateTime(2023, 1, 2), + Value = 123 + }; + + var object2 = new MyClassWithDates + { + Name = "Test", + CreatedDate = new DateTime(2024, 6, 15), + ModifiedDate = new DateTime(2024, 7, 20), + Value = 123 + }; + + await TUnitAssert.That(object1) + .IsNotEquivalentTo(object2) + .IgnoringType(); + } + + [Test] + public async Task IgnoringType_Without_Matching_Type_Still_Compares_All_Properties() + { + var object1 = new MyClassWithoutDates + { + Name = "Test", + Value = 123 + }; + + var object2 = new MyClassWithoutDates + { + Name = "Different", + Value = 123 + }; + + await TUnitAssert.That(object1) + .IsNotEquivalentTo(object2) + .IgnoringType(); + } + + private class MyClassWithDates + { + public string Name { get; set; } = string.Empty; + public DateTime CreatedDate { get; set; } + public DateTime ModifiedDate { get; set; } + public int Value { get; set; } + } + + private class MyClassWithNullableDates + { + public string Name { get; set; } = string.Empty; + public DateTime? OptionalDate { get; set; } + public int Value { get; set; } + } + + private class MyClassWithDateTimeOffset + { + public string Name { get; set; } = string.Empty; + public DateTimeOffset Timestamp { get; set; } + public int Value { get; set; } + } + + private class MyClassWithMultipleTypes + { + public string Name { get; set; } = string.Empty; + public DateTime CreatedDate { get; set; } + public Guid Guid { get; set; } + public int Value { get; set; } + } + + private class MyClassWithDateFields + { + public string Name { get; set; } = string.Empty; + public DateTime CreatedDateField; + public int Value { get; set; } + } + + private class MyClassWithoutDates + { + public string Name { get; set; } = string.Empty; + public int Value { get; set; } + } +} \ No newline at end of file diff --git a/TUnit.Assertions/AssertionBuilders/Wrappers/EquivalentToAssertionBuilderWrapper.cs b/TUnit.Assertions/AssertionBuilders/Wrappers/EquivalentToAssertionBuilderWrapper.cs index 12dfc6db1d..b5fc2f0903 100644 --- a/TUnit.Assertions/AssertionBuilders/Wrappers/EquivalentToAssertionBuilderWrapper.cs +++ b/TUnit.Assertions/AssertionBuilders/Wrappers/EquivalentToAssertionBuilderWrapper.cs @@ -21,6 +21,28 @@ public EquivalentToAssertionBuilderWrapper IgnoringMember(st return this; } + public EquivalentToAssertionBuilderWrapper IgnoringType() + { + var assertion = (EquivalentToExpectedValueAssertCondition) Assertions.Peek(); + + assertion.IgnoringType(typeof(TType)); + + AppendCallerMethod([$"<{typeof(TType).Name}>"]); + + return this; + } + + public EquivalentToAssertionBuilderWrapper IgnoringType(Type type, [CallerArgumentExpression(nameof(type))] string doNotPopulateThis = "") + { + var assertion = (EquivalentToExpectedValueAssertCondition) Assertions.Peek(); + + assertion.IgnoringType(type); + + AppendCallerMethod([doNotPopulateThis]); + + return this; + } + public EquivalentToAssertionBuilderWrapper WithPartialEquivalency() { var assertion = (EquivalentToExpectedValueAssertCondition) Assertions.Peek(); diff --git a/TUnit.Assertions/AssertionBuilders/Wrappers/NotEquivalentToAssertionBuilderWrapper.cs b/TUnit.Assertions/AssertionBuilders/Wrappers/NotEquivalentToAssertionBuilderWrapper.cs index 46d08f7826..7aed5771a5 100644 --- a/TUnit.Assertions/AssertionBuilders/Wrappers/NotEquivalentToAssertionBuilderWrapper.cs +++ b/TUnit.Assertions/AssertionBuilders/Wrappers/NotEquivalentToAssertionBuilderWrapper.cs @@ -21,6 +21,28 @@ public NotEquivalentToAssertionBuilderWrapper IgnoringMember return this; } + public NotEquivalentToAssertionBuilderWrapper IgnoringType() + { + var assertion = (NotEquivalentToExpectedValueAssertCondition) Assertions.Peek(); + + assertion.IgnoringType(typeof(TType)); + + AppendCallerMethod([$"<{typeof(TType).Name}>"]); + + return this; + } + + public NotEquivalentToAssertionBuilderWrapper IgnoringType(Type type, [CallerArgumentExpression(nameof(type))] string doNotPopulateThis = "") + { + var assertion = (NotEquivalentToExpectedValueAssertCondition) Assertions.Peek(); + + assertion.IgnoringType(type); + + AppendCallerMethod([doNotPopulateThis]); + + return this; + } + public NotEquivalentToAssertionBuilderWrapper WithPartialEquivalency() { var assertion = (NotEquivalentToExpectedValueAssertCondition) Assertions.Peek(); diff --git a/TUnit.Assertions/Assertions/Generics/Conditions/EquivalentToExpectedValueAssertCondition.cs b/TUnit.Assertions/Assertions/Generics/Conditions/EquivalentToExpectedValueAssertCondition.cs index edde2b9203..98646d1d5b 100644 --- a/TUnit.Assertions/Assertions/Generics/Conditions/EquivalentToExpectedValueAssertCondition.cs +++ b/TUnit.Assertions/Assertions/Generics/Conditions/EquivalentToExpectedValueAssertCondition.cs @@ -14,6 +14,7 @@ public class EquivalentToExpectedValueAssertCondition< TExpected>(TExpected expected, string? expectedExpression) : ExpectedValueAssertCondition(expected) { private readonly List _ignoredMembers = []; + private readonly List _ignoredTypes = []; public EquivalencyKind EquivalencyKind { get; set; } = EquivalencyKind.Full; @@ -62,6 +63,7 @@ protected override ValueTask GetResult(TActual? actualValue, TE var failures = Compare.CheckEquivalent(actualValue, ExpectedValue, new CompareOptions { MembersToIgnore = [.. _ignoredMembers], + TypesToIgnore = [.. _ignoredTypes], EquivalencyKind = EquivalencyKind }, null).ToList(); @@ -95,4 +97,9 @@ public void IgnoringMember(string fieldName) { _ignoredMembers.Add(fieldName); } + + public void IgnoringType(Type type) + { + _ignoredTypes.Add(type); + } } diff --git a/TUnit.Assertions/Assertions/Generics/Conditions/NotEquivalentToExpectedValueAssertCondition.cs b/TUnit.Assertions/Assertions/Generics/Conditions/NotEquivalentToExpectedValueAssertCondition.cs index bb13c99230..fe7c74ebf0 100644 --- a/TUnit.Assertions/Assertions/Generics/Conditions/NotEquivalentToExpectedValueAssertCondition.cs +++ b/TUnit.Assertions/Assertions/Generics/Conditions/NotEquivalentToExpectedValueAssertCondition.cs @@ -15,6 +15,7 @@ public class NotEquivalentToExpectedValueAssertCondition< TExpected>(TExpected expected, string? expectedExpression) : ExpectedValueAssertCondition(expected) { private readonly List _ignoredMembers = []; + private readonly List _ignoredTypes = []; public EquivalencyKind EquivalencyKind { get; set; } = EquivalencyKind.Full; @@ -45,6 +46,7 @@ protected override ValueTask GetResult(TActual? actualValue, TE new CompareOptions { MembersToIgnore = [.. _ignoredMembers], + TypesToIgnore = [.. _ignoredTypes], EquivalencyKind = EquivalencyKind, }); @@ -81,6 +83,7 @@ protected override ValueTask GetResult(TActual? actualValue, TE var failures = Compare.CheckEquivalent(actualValue, ExpectedValue, new CompareOptions { MembersToIgnore = [.. _ignoredMembers], + TypesToIgnore = [.. _ignoredTypes], EquivalencyKind = EquivalencyKind }, null).ToList(); @@ -101,4 +104,9 @@ public void IgnoringMember(string fieldName) { _ignoredMembers.Add(fieldName); } + + public void IgnoringType(Type type) + { + _ignoredTypes.Add(type); + } } diff --git a/TUnit.Assertions/Compare.cs b/TUnit.Assertions/Compare.cs index 719c87885a..52a366e3bd 100644 --- a/TUnit.Assertions/Compare.cs +++ b/TUnit.Assertions/Compare.cs @@ -172,6 +172,17 @@ private static IEnumerable CheckEquivalent< var actualFieldInfo = actual.GetType().GetField(fieldName, BindingFlags); var expectedFieldInfo = expected.GetType().GetField(fieldName, BindingFlags); + // Check if field type should be ignored + if (actualFieldInfo != null && ShouldIgnoreType(actualFieldInfo.FieldType, options.TypesToIgnore)) + { + continue; + } + + if (expectedFieldInfo != null && ShouldIgnoreType(expectedFieldInfo.FieldType, options.TypesToIgnore)) + { + continue; + } + if (options.EquivalencyKind == EquivalencyKind.Partial && expectedFieldInfo is null) { continue; @@ -221,6 +232,17 @@ private static IEnumerable CheckEquivalent< var actualPropertyInfo = actual.GetType().GetProperty(propertyName, BindingFlags); var expectedPropertyInfo = expected.GetType().GetProperty(propertyName, BindingFlags); + // Check if property type should be ignored + if (actualPropertyInfo != null && ShouldIgnoreType(actualPropertyInfo.PropertyType, options.TypesToIgnore)) + { + continue; + } + + if (expectedPropertyInfo != null && ShouldIgnoreType(expectedPropertyInfo.PropertyType, options.TypesToIgnore)) + { + continue; + } + if (options.EquivalencyKind == EquivalencyKind.Partial && expectedPropertyInfo is null) { continue; @@ -272,4 +294,22 @@ private static string InitialMemberName(object? actual, int? index) return $"{type}[{index}]"; } + + private static bool ShouldIgnoreType(Type type, Type[] typesToIgnore) + { + if (typesToIgnore.Length == 0) + { + return false; + } + + // Get the underlying type if it's nullable + var underlyingType = Nullable.GetUnderlyingType(type) ?? type; + + // Check if the type or its underlying type (for nullable) should be ignored + return typesToIgnore.Any(ignoredType => + type == ignoredType || + underlyingType == ignoredType || + type.IsAssignableFrom(ignoredType) || + underlyingType.IsAssignableFrom(ignoredType)); + } } diff --git a/TUnit.Assertions/CompareOptions.cs b/TUnit.Assertions/CompareOptions.cs index 9d14818af7..f6e3712496 100644 --- a/TUnit.Assertions/CompareOptions.cs +++ b/TUnit.Assertions/CompareOptions.cs @@ -5,5 +5,6 @@ namespace TUnit.Assertions; public record CompareOptions { public string[] MembersToIgnore { get; init; } = []; + public Type[] TypesToIgnore { get; init; } = []; public EquivalencyKind EquivalencyKind { get; set; } = EquivalencyKind.Full; } 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 bc2ade45fb..4093e641bc 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 @@ -80,6 +80,7 @@ namespace public CompareOptions() { } public . EquivalencyKind { get; set; } public string[] MembersToIgnore { get; init; } + public [] TypesToIgnore { get; init; } } public class ComparisonFailure : <.ComparisonFailure> { @@ -831,6 +832,8 @@ namespace . public class EquivalentToAssertionBuilderWrapper : . { public ..EquivalentToAssertionBuilderWrapper IgnoringMember(string propertyName, [.("propertyName")] string doNotPopulateThis = "") { } + public ..EquivalentToAssertionBuilderWrapper IgnoringType( type, [.("type")] string doNotPopulateThis = "") { } + public ..EquivalentToAssertionBuilderWrapper IgnoringType() { } public ..EquivalentToAssertionBuilderWrapper WithPartialEquivalency() { } } public class GenericEqualToAssertionBuilderWrapper : . { } @@ -844,6 +847,8 @@ namespace . public class NotEquivalentToAssertionBuilderWrapper : . { public ..NotEquivalentToAssertionBuilderWrapper IgnoringMember(string propertyName, [.("propertyName")] string doNotPopulateThis = "") { } + public ..NotEquivalentToAssertionBuilderWrapper IgnoringType( type, [.("type")] string doNotPopulateThis = "") { } + public ..NotEquivalentToAssertionBuilderWrapper IgnoringType() { } public ..NotEquivalentToAssertionBuilderWrapper WithPartialEquivalency() { } } public class NotNullAssertionBuilderWrapper : . @@ -1030,6 +1035,7 @@ namespace ..Conditions protected override string GetExpectation() { } protected override .<.> GetResult(TActual? actualValue, TExpected? expectedValue) { } public void IgnoringMember(string fieldName) { } + public void IgnoringType( type) { } } public class NotAssignableFromExpectedValueAssertCondition : . { @@ -1062,6 +1068,7 @@ namespace ..Conditions protected override string GetExpectation() { } protected override .<.> GetResult(TActual? actualValue, TExpected? expectedValue) { } public void IgnoringMember(string fieldName) { } + public void IgnoringType( type) { } } public class NotSameReferenceExpectedValueAssertCondition : . { 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 71ae453557..64b4574e68 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 @@ -80,6 +80,7 @@ namespace public CompareOptions() { } public . EquivalencyKind { get; set; } public string[] MembersToIgnore { get; init; } + public [] TypesToIgnore { get; init; } } public class ComparisonFailure : <.ComparisonFailure> { @@ -831,6 +832,8 @@ namespace . public class EquivalentToAssertionBuilderWrapper : . { public ..EquivalentToAssertionBuilderWrapper IgnoringMember(string propertyName, [.("propertyName")] string doNotPopulateThis = "") { } + public ..EquivalentToAssertionBuilderWrapper IgnoringType( type, [.("type")] string doNotPopulateThis = "") { } + public ..EquivalentToAssertionBuilderWrapper IgnoringType() { } public ..EquivalentToAssertionBuilderWrapper WithPartialEquivalency() { } } public class GenericEqualToAssertionBuilderWrapper : . { } @@ -844,6 +847,8 @@ namespace . public class NotEquivalentToAssertionBuilderWrapper : . { public ..NotEquivalentToAssertionBuilderWrapper IgnoringMember(string propertyName, [.("propertyName")] string doNotPopulateThis = "") { } + public ..NotEquivalentToAssertionBuilderWrapper IgnoringType( type, [.("type")] string doNotPopulateThis = "") { } + public ..NotEquivalentToAssertionBuilderWrapper IgnoringType() { } public ..NotEquivalentToAssertionBuilderWrapper WithPartialEquivalency() { } } public class NotNullAssertionBuilderWrapper : . @@ -1030,6 +1035,7 @@ namespace ..Conditions protected override string GetExpectation() { } protected override .<.> GetResult(TActual? actualValue, TExpected? expectedValue) { } public void IgnoringMember(string fieldName) { } + public void IgnoringType( type) { } } public class NotAssignableFromExpectedValueAssertCondition : . { @@ -1062,6 +1068,7 @@ namespace ..Conditions protected override string GetExpectation() { } protected override .<.> GetResult(TActual? actualValue, TExpected? expectedValue) { } public void IgnoringMember(string fieldName) { } + public void IgnoringType( type) { } } public class NotSameReferenceExpectedValueAssertCondition : . { 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 59e3aaff47..4f9cdc57f3 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 @@ -79,6 +79,7 @@ namespace public CompareOptions() { } public . EquivalencyKind { get; set; } public string[] MembersToIgnore { get; init; } + public [] TypesToIgnore { get; init; } } public class ComparisonFailure : <.ComparisonFailure> { @@ -802,6 +803,8 @@ namespace . public class EquivalentToAssertionBuilderWrapper : . { public ..EquivalentToAssertionBuilderWrapper IgnoringMember(string propertyName, [.("propertyName")] string doNotPopulateThis = "") { } + public ..EquivalentToAssertionBuilderWrapper IgnoringType( type, [.("type")] string doNotPopulateThis = "") { } + public ..EquivalentToAssertionBuilderWrapper IgnoringType() { } public ..EquivalentToAssertionBuilderWrapper WithPartialEquivalency() { } } public class GenericEqualToAssertionBuilderWrapper : . { } @@ -815,6 +818,8 @@ namespace . public class NotEquivalentToAssertionBuilderWrapper : . { public ..NotEquivalentToAssertionBuilderWrapper IgnoringMember(string propertyName, [.("propertyName")] string doNotPopulateThis = "") { } + public ..NotEquivalentToAssertionBuilderWrapper IgnoringType( type, [.("type")] string doNotPopulateThis = "") { } + public ..NotEquivalentToAssertionBuilderWrapper IgnoringType() { } public ..NotEquivalentToAssertionBuilderWrapper WithPartialEquivalency() { } } public class NotNullAssertionBuilderWrapper : . @@ -997,6 +1002,7 @@ namespace ..Conditions protected override string GetExpectation() { } protected override .<.> GetResult(TActual? actualValue, TExpected? expectedValue) { } public void IgnoringMember(string fieldName) { } + public void IgnoringType( type) { } } public class NotAssignableFromExpectedValueAssertCondition : . { @@ -1029,6 +1035,7 @@ namespace ..Conditions protected override string GetExpectation() { } protected override .<.> GetResult(TActual? actualValue, TExpected? expectedValue) { } public void IgnoringMember(string fieldName) { } + public void IgnoringType( type) { } } public class NotSameReferenceExpectedValueAssertCondition : . {