From fd300a0d53309479549832ba1e75b3314fba1cd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Wed, 3 Sep 2025 15:23:34 +0200 Subject: [PATCH 1/2] feat: improve ignoring members for equivalency --- .../EquivalencyComparison.Compare.cs | 56 +++---- .../Equivalency/EquivalencyTypeOptions.cs | 3 +- .../Equivalency/MemberToIgnore.cs | 41 +++++ .../EquivalencyOptionsExtensions.cs | 79 +++++++++- .../Expected/aweXpect_net8.0.txt | 8 + .../Expected/aweXpect_netstandard2.0.txt | 8 + .../Expected/aweXpect.Core_net8.0.txt | 19 ++- .../Expected/aweXpect.Core_netstandard2.0.txt | 19 ++- .../EquivalencyOptionsExtensionsTests.cs | 88 ++++++++++- .../ThatObject.IsEquivalentTo.Tests.cs | 147 +++++++++++++++++- .../ThatObject.IsNotEquivalentTo.Tests.cs | 18 ++- Tests/aweXpect.Tests/Objects/ThatObject.cs | 2 + 12 files changed, 452 insertions(+), 36 deletions(-) create mode 100644 Source/aweXpect.Core/Equivalency/MemberToIgnore.cs diff --git a/Source/aweXpect.Core/Equivalency/EquivalencyComparison.Compare.cs b/Source/aweXpect.Core/Equivalency/EquivalencyComparison.Compare.cs index 0a1c1da99..9e7d86bee 100644 --- a/Source/aweXpect.Core/Equivalency/EquivalencyComparison.Compare.cs +++ b/Source/aweXpect.Core/Equivalency/EquivalencyComparison.Compare.cs @@ -198,19 +198,20 @@ private static async Task if (typeOptions.Fields != IncludeMembers.None) { BindingFlags fieldBindingFlags = typeOptions.Fields.GetBindingFlags(); - foreach (string fieldName in expected.GetType().GetFields(typeOptions.Fields).Select(x => x.Name)) + foreach (FieldInfo? fieldInfo in expected.GetType().GetFields(typeOptions.Fields)) { memberCount++; - string fieldMemberPath = ConcatMemberPath(memberPath, fieldName); - if (typeOptions.MembersToIgnore.Contains(fieldMemberPath)) + string fieldMemberPath = ConcatMemberPath(memberPath, fieldInfo.Name); + if (typeOptions.MembersToIgnore.Any(memberToIgnore + => memberToIgnore.IgnoreMember(fieldMemberPath, fieldInfo.FieldType, fieldInfo))) { continue; } object? actualFieldValue = - actual.GetType().GetField(fieldName, fieldBindingFlags)?.GetValue(actual); + actual.GetType().GetField(fieldInfo.Name, fieldBindingFlags)?.GetValue(actual); object? expectedFieldValue = - expected.GetType().GetField(fieldName, fieldBindingFlags)?.GetValue(expected); + expected.GetType().GetField(fieldInfo.Name, fieldBindingFlags)?.GetValue(expected); if (!await Compare(actualFieldValue, expectedFieldValue, options, options.GetTypeOptions(actualFieldValue?.GetType(), typeOptions), @@ -224,20 +225,20 @@ private static async Task if (typeOptions.Properties != IncludeMembers.None) { BindingFlags propertyBindingFlags = typeOptions.Properties.GetBindingFlags(); - foreach (string propertyName in expected.GetType().GetProperties(typeOptions.Properties) - .Select(x => x.Name)) + foreach (PropertyInfo? propertyInfo in expected.GetType().GetProperties(typeOptions.Properties)) { memberCount++; - string propertyMemberPath = ConcatMemberPath(memberPath, propertyName); - if (typeOptions.MembersToIgnore.Contains(propertyMemberPath)) + string propertyMemberPath = ConcatMemberPath(memberPath, propertyInfo.Name); + if (typeOptions.MembersToIgnore.Any(memberToIgnore + => memberToIgnore.IgnoreMember(propertyMemberPath, propertyInfo.PropertyType, propertyInfo))) { continue; } - object? actualPropertyValue = actual.GetType().GetProperty(propertyName, propertyBindingFlags) + object? actualPropertyValue = actual.GetType().GetProperty(propertyInfo.Name, propertyBindingFlags) ?.GetValue(actual); object? expectedPropertyValue = - expected.GetType().GetProperty(propertyName, propertyBindingFlags)?.GetValue(expected); + expected.GetType().GetProperty(propertyInfo.Name, propertyBindingFlags)?.GetValue(expected); if (!await Compare(actualPropertyValue, expectedPropertyValue, options, options.GetTypeOptions(actualPropertyValue?.GetType(), typeOptions), @@ -287,12 +288,13 @@ private static async Task foreach (object? key in actual.Keys) { string elementMemberPath = $"{memberPath}[{key}]"; - if (typeOptions.MembersToIgnore.Contains(elementMemberPath)) + + object? actualObject = actual[key]; + if (typeOptions.MembersToIgnore.Any(memberToIgnore + => memberToIgnore.IgnoreMember(elementMemberPath, actualObject?.GetType() ?? typeof(object), null))) { continue; } - - object? actualObject = actual[key]; if (expected.Contains(key)) { object? expectedObject = expected[key]; @@ -328,14 +330,13 @@ private static async Task } string elementMemberPath = $"{memberPath}[{key}]"; - if (typeOptions.MembersToIgnore.Contains(elementMemberPath)) + object? expectedObject = expected[key]; + if (typeOptions.MembersToIgnore.Any(memberToIgnore + => memberToIgnore.IgnoreMember(elementMemberPath, expectedObject?.GetType() ?? typeof(object), null))) { continue; } - - object? expectedObject = expected[key]; - failureBuilder.AppendLine(); if (failureBuilder.Length > 2) { @@ -385,12 +386,13 @@ private static async Task for (int i = 0; i < Math.Min(actualObjects.Length, expectedObjects.Length); i++) { string elementMemberPath = $"{memberPath}[{(keys is null ? i : keys[i])}]"; - if (typeOptions.MembersToIgnore.Contains(elementMemberPath)) + object? actualObject = actualObjects.ElementAtOrDefault(i); + if (typeOptions.MembersToIgnore.Any(memberToIgnore + => memberToIgnore.IgnoreMember(elementMemberPath, actualObject?.GetType() ?? typeof(object), null))) { continue; } - - object? actualObject = actualObjects.ElementAtOrDefault(i); + object? expectedObject = expectedObjects.ElementAtOrDefault(i); if (!await Compare(actualObject, expectedObject, @@ -406,13 +408,13 @@ private static async Task for (int i = actualObjects.Length; i < expectedObjects.Length; i++) { string elementMemberPath = $"{memberPath}[{i}]"; - if (typeOptions.MembersToIgnore.Contains(elementMemberPath)) + object? expectedObject = expectedObjects.ElementAtOrDefault(i); + if (typeOptions.MembersToIgnore.Any(memberToIgnore + => memberToIgnore.IgnoreMember(elementMemberPath, expectedObject?.GetType() ?? typeof(object), null))) { continue; } - object? expectedObject = expectedObjects.ElementAtOrDefault(i); - failureBuilder.AppendLine(); if (failureBuilder.Length > 2) { @@ -432,13 +434,13 @@ private static async Task for (int i = expectedObjects.Length; i < actualObjects.Length; i++) { string elementMemberPath = $"{memberPath}[{(keys is null ? i : keys[i])}]"; - if (typeOptions.MembersToIgnore.Contains(elementMemberPath)) + object? actualObject = actualObjects.ElementAtOrDefault(i); + if (typeOptions.MembersToIgnore.Any(memberToIgnore + => memberToIgnore.IgnoreMember(elementMemberPath, actualObject?.GetType() ?? typeof(object), null))) { continue; } - object? actualObject = actualObjects.ElementAtOrDefault(i); - failureBuilder.AppendLine(); if (failureBuilder.Length > 2) { diff --git a/Source/aweXpect.Core/Equivalency/EquivalencyTypeOptions.cs b/Source/aweXpect.Core/Equivalency/EquivalencyTypeOptions.cs index 2c56fc319..2153283a3 100644 --- a/Source/aweXpect.Core/Equivalency/EquivalencyTypeOptions.cs +++ b/Source/aweXpect.Core/Equivalency/EquivalencyTypeOptions.cs @@ -1,3 +1,4 @@ +using System.Linq; using System.Text; namespace aweXpect.Equivalency; @@ -18,7 +19,7 @@ public record EquivalencyTypeOptions /// /// The members that should be ignored when checking for equivalency. /// - public string[] MembersToIgnore { get; init; } = []; + public MemberToIgnore[] MembersToIgnore { get; init; } = []; /// /// Specifies which fields to include in the object comparison. diff --git a/Source/aweXpect.Core/Equivalency/MemberToIgnore.cs b/Source/aweXpect.Core/Equivalency/MemberToIgnore.cs new file mode 100644 index 000000000..50354c1d3 --- /dev/null +++ b/Source/aweXpect.Core/Equivalency/MemberToIgnore.cs @@ -0,0 +1,41 @@ +using System; +using System.Reflection; + +namespace aweXpect.Equivalency; + +/// +/// Class to specify which members to ignore. +/// +public abstract class MemberToIgnore +{ + /// + /// Checks if the member should be ignored. + /// + public abstract bool IgnoreMember(string memberPath, Type memberType, MemberInfo? memberInfo); + + /// + /// Ignores all members that satisfy the . + /// + public class ByPredicate(Func predicate, string description) : MemberToIgnore + { + /// + public override bool IgnoreMember(string memberPath, Type memberType, MemberInfo? memberInfo) + => predicate(memberPath, memberType, memberInfo); + + /// + public override string ToString() => description; + } + + /// + /// Ignores all members that have the provided . + /// + public class ByName(string memberName) : MemberToIgnore + { + /// + public override bool IgnoreMember(string memberPath, Type memberType, MemberInfo? memberInfo) + => memberPath.EndsWith(memberName, StringComparison.OrdinalIgnoreCase); + + /// + public override string ToString() => $"\"{memberName}\""; + } +} diff --git a/Source/aweXpect/Equivalency/EquivalencyOptionsExtensions.cs b/Source/aweXpect/Equivalency/EquivalencyOptionsExtensions.cs index 55cff1487..2eb99a884 100644 --- a/Source/aweXpect/Equivalency/EquivalencyOptionsExtensions.cs +++ b/Source/aweXpect/Equivalency/EquivalencyOptionsExtensions.cs @@ -1,4 +1,6 @@ using System; +using System.Reflection; +using System.Runtime.CompilerServices; using aweXpect.Customization; namespace aweXpect.Equivalency; @@ -17,7 +19,82 @@ public static TEquivalencyOptions IgnoringMember( where TEquivalencyOptions : EquivalencyTypeOptions => @this with { - MembersToIgnore = [..@this.MembersToIgnore, memberToIgnore,], + MembersToIgnore = [..@this.MembersToIgnore, new MemberToIgnore.ByName(memberToIgnore),], + }; + + /// + /// Ignores members matching the when checking for equivalency. + /// + public static TEquivalencyOptions Ignoring( + this TEquivalencyOptions @this, + Func predicate, + [CallerArgumentExpression("predicate")] + string doNotPopulateThisValue = "") + where TEquivalencyOptions : EquivalencyTypeOptions + => @this with + { + MembersToIgnore = + [ + ..@this.MembersToIgnore, + new MemberToIgnore.ByPredicate((memberName, memberType, _) => predicate(memberName, memberType), + doNotPopulateThisValue), + ], + }; + + /// + /// Ignores members matching the when checking for equivalency. + /// + public static TEquivalencyOptions Ignoring( + this TEquivalencyOptions @this, + Func predicate, + [CallerArgumentExpression("predicate")] + string doNotPopulateThisValue = "") + where TEquivalencyOptions : EquivalencyTypeOptions + => @this with + { + MembersToIgnore = + [ + ..@this.MembersToIgnore, + new MemberToIgnore.ByPredicate((memberName, _, _) => predicate(memberName), + doNotPopulateThisValue), + ], + }; + + /// + /// Ignores members matching the when checking for equivalency. + /// + public static TEquivalencyOptions Ignoring( + this TEquivalencyOptions @this, + Func predicate, + [CallerArgumentExpression("predicate")] + string doNotPopulateThisValue = "") + where TEquivalencyOptions : EquivalencyTypeOptions + => @this with + { + MembersToIgnore = + [ + ..@this.MembersToIgnore, + new MemberToIgnore.ByPredicate((_, memberType, _) => predicate(memberType), + doNotPopulateThisValue), + ], + }; + + /// + /// Ignores members matching the when checking for equivalency. + /// + public static TEquivalencyOptions Ignoring( + this TEquivalencyOptions @this, + Func predicate, + [CallerArgumentExpression("predicate")] + string doNotPopulateThisValue = "") + where TEquivalencyOptions : EquivalencyTypeOptions + => @this with + { + MembersToIgnore = + [ + ..@this.MembersToIgnore, + new MemberToIgnore.ByPredicate(predicate, doNotPopulateThisValue), + ], }; /// diff --git a/Tests/aweXpect.Api.Tests/Expected/aweXpect_net8.0.txt b/Tests/aweXpect.Api.Tests/Expected/aweXpect_net8.0.txt index 43c593949..5a96aa35b 100644 --- a/Tests/aweXpect.Api.Tests/Expected/aweXpect_net8.0.txt +++ b/Tests/aweXpect.Api.Tests/Expected/aweXpect_net8.0.txt @@ -1236,6 +1236,14 @@ namespace aweXpect.Equivalency } public static class EquivalencyOptionsExtensions { + public static TEquivalencyOptions Ignoring(this TEquivalencyOptions @this, System.Func predicate, [System.Runtime.CompilerServices.CallerArgumentExpression("predicate")] string doNotPopulateThisValue = "") + where TEquivalencyOptions : aweXpect.Equivalency.EquivalencyTypeOptions { } + public static TEquivalencyOptions Ignoring(this TEquivalencyOptions @this, System.Func predicate, [System.Runtime.CompilerServices.CallerArgumentExpression("predicate")] string doNotPopulateThisValue = "") + where TEquivalencyOptions : aweXpect.Equivalency.EquivalencyTypeOptions { } + public static TEquivalencyOptions Ignoring(this TEquivalencyOptions @this, System.Func predicate, [System.Runtime.CompilerServices.CallerArgumentExpression("predicate")] string doNotPopulateThisValue = "") + where TEquivalencyOptions : aweXpect.Equivalency.EquivalencyTypeOptions { } + public static TEquivalencyOptions Ignoring(this TEquivalencyOptions @this, System.Func predicate, [System.Runtime.CompilerServices.CallerArgumentExpression("predicate")] string doNotPopulateThisValue = "") + where TEquivalencyOptions : aweXpect.Equivalency.EquivalencyTypeOptions { } public static TEquivalencyOptions IgnoringCollectionOrder(this TEquivalencyOptions @this, bool ignoreCollectionOrder = true) where TEquivalencyOptions : aweXpect.Equivalency.EquivalencyTypeOptions { } public static TEquivalencyOptions IgnoringMember(this TEquivalencyOptions @this, string memberToIgnore) diff --git a/Tests/aweXpect.Api.Tests/Expected/aweXpect_netstandard2.0.txt b/Tests/aweXpect.Api.Tests/Expected/aweXpect_netstandard2.0.txt index f319217a3..60b8d470a 100644 --- a/Tests/aweXpect.Api.Tests/Expected/aweXpect_netstandard2.0.txt +++ b/Tests/aweXpect.Api.Tests/Expected/aweXpect_netstandard2.0.txt @@ -1182,6 +1182,14 @@ namespace aweXpect.Equivalency } public static class EquivalencyOptionsExtensions { + public static TEquivalencyOptions Ignoring(this TEquivalencyOptions @this, System.Func predicate, [System.Runtime.CompilerServices.CallerArgumentExpression("predicate")] string doNotPopulateThisValue = "") + where TEquivalencyOptions : aweXpect.Equivalency.EquivalencyTypeOptions { } + public static TEquivalencyOptions Ignoring(this TEquivalencyOptions @this, System.Func predicate, [System.Runtime.CompilerServices.CallerArgumentExpression("predicate")] string doNotPopulateThisValue = "") + where TEquivalencyOptions : aweXpect.Equivalency.EquivalencyTypeOptions { } + public static TEquivalencyOptions Ignoring(this TEquivalencyOptions @this, System.Func predicate, [System.Runtime.CompilerServices.CallerArgumentExpression("predicate")] string doNotPopulateThisValue = "") + where TEquivalencyOptions : aweXpect.Equivalency.EquivalencyTypeOptions { } + public static TEquivalencyOptions Ignoring(this TEquivalencyOptions @this, System.Func predicate, [System.Runtime.CompilerServices.CallerArgumentExpression("predicate")] string doNotPopulateThisValue = "") + where TEquivalencyOptions : aweXpect.Equivalency.EquivalencyTypeOptions { } public static TEquivalencyOptions IgnoringCollectionOrder(this TEquivalencyOptions @this, bool ignoreCollectionOrder = true) where TEquivalencyOptions : aweXpect.Equivalency.EquivalencyTypeOptions { } public static TEquivalencyOptions IgnoringMember(this TEquivalencyOptions @this, string memberToIgnore) diff --git a/Tests/aweXpect.Core.Api.Tests/Expected/aweXpect.Core_net8.0.txt b/Tests/aweXpect.Core.Api.Tests/Expected/aweXpect.Core_net8.0.txt index fe3b8fd56..6ddc0b724 100644 --- a/Tests/aweXpect.Core.Api.Tests/Expected/aweXpect.Core_net8.0.txt +++ b/Tests/aweXpect.Core.Api.Tests/Expected/aweXpect.Core_net8.0.txt @@ -568,7 +568,7 @@ namespace aweXpect.Equivalency public aweXpect.Equivalency.EquivalencyComparisonType? ComparisonType { get; init; } public aweXpect.Equivalency.IncludeMembers Fields { get; init; } public bool IgnoreCollectionOrder { get; init; } - public string[] MembersToIgnore { get; init; } + public aweXpect.Equivalency.MemberToIgnore[] MembersToIgnore { get; init; } public aweXpect.Equivalency.IncludeMembers Properties { get; init; } } [System.Flags] @@ -590,6 +590,23 @@ namespace aweXpect.Equivalency public override string? ToString() { } } } + public abstract class MemberToIgnore + { + protected MemberToIgnore() { } + public abstract bool IgnoreMember(string memberPath, System.Type memberType, System.Reflection.MemberInfo? memberInfo); + public class ByName : aweXpect.Equivalency.MemberToIgnore + { + public ByName(string memberName) { } + public override bool IgnoreMember(string memberPath, System.Type memberType, System.Reflection.MemberInfo? memberInfo) { } + public override string ToString() { } + } + public class ByPredicate : aweXpect.Equivalency.MemberToIgnore + { + public ByPredicate(System.Func predicate, string description) { } + public override bool IgnoreMember(string memberPath, System.Type memberType, System.Reflection.MemberInfo? memberInfo) { } + public override string ToString() { } + } + } } namespace aweXpect { diff --git a/Tests/aweXpect.Core.Api.Tests/Expected/aweXpect.Core_netstandard2.0.txt b/Tests/aweXpect.Core.Api.Tests/Expected/aweXpect.Core_netstandard2.0.txt index d0be10ada..7fc520898 100644 --- a/Tests/aweXpect.Core.Api.Tests/Expected/aweXpect.Core_netstandard2.0.txt +++ b/Tests/aweXpect.Core.Api.Tests/Expected/aweXpect.Core_netstandard2.0.txt @@ -554,7 +554,7 @@ namespace aweXpect.Equivalency public aweXpect.Equivalency.EquivalencyComparisonType? ComparisonType { get; init; } public aweXpect.Equivalency.IncludeMembers Fields { get; init; } public bool IgnoreCollectionOrder { get; init; } - public string[] MembersToIgnore { get; init; } + public aweXpect.Equivalency.MemberToIgnore[] MembersToIgnore { get; init; } public aweXpect.Equivalency.IncludeMembers Properties { get; init; } } [System.Flags] @@ -576,6 +576,23 @@ namespace aweXpect.Equivalency public override string? ToString() { } } } + public abstract class MemberToIgnore + { + protected MemberToIgnore() { } + public abstract bool IgnoreMember(string memberPath, System.Type memberType, System.Reflection.MemberInfo? memberInfo); + public class ByName : aweXpect.Equivalency.MemberToIgnore + { + public ByName(string memberName) { } + public override bool IgnoreMember(string memberPath, System.Type memberType, System.Reflection.MemberInfo? memberInfo) { } + public override string ToString() { } + } + public class ByPredicate : aweXpect.Equivalency.MemberToIgnore + { + public ByPredicate(System.Func predicate, string description) { } + public override bool IgnoreMember(string memberPath, System.Type memberType, System.Reflection.MemberInfo? memberInfo) { } + public override string ToString() { } + } + } } namespace aweXpect { diff --git a/Tests/aweXpect.Core.Tests/Equivalency/EquivalencyOptionsExtensionsTests.cs b/Tests/aweXpect.Core.Tests/Equivalency/EquivalencyOptionsExtensionsTests.cs index ec0bd673d..8f496a9cf 100644 --- a/Tests/aweXpect.Core.Tests/Equivalency/EquivalencyOptionsExtensionsTests.cs +++ b/Tests/aweXpect.Core.Tests/Equivalency/EquivalencyOptionsExtensionsTests.cs @@ -4,6 +4,92 @@ namespace aweXpect.Core.Tests.Equivalency; public sealed class EquivalencyOptionsExtensionsTests { + [Fact] + public async Task Generic_For_Ignoring_StringAndTypePredicate_ShouldSetOptionForType() + { + EquivalencyOptions options = new(); + + EquivalencyOptions result = + options.For(o => o.Ignoring((n, t) => n.EndsWith("At") && t == typeof(DateTime))); + + await That(result.MembersToIgnore).IsEmpty(); + await That(result.CustomOptions).ContainsKey(typeof(MyClass)) + .WhoseValue.IsEquivalentTo(new + { + MembersToIgnore = It.Is().That.HasCount(1), + }); + await That(result.ToString()).IsEqualTo(""" + - include public fields and properties + - for EquivalencyOptionsExtensionsTests.MyClass: + - include public fields and properties + - ignore members: [(n, t) => n.EndsWith("At") && t == typeof(DateTime)] + """); + } + + [Fact] + public async Task Generic_For_Ignoring_StringPredicate_ShouldSetOptionForType() + { + EquivalencyOptions options = new(); + + EquivalencyOptions result = options.For(o => o.Ignoring(x => x == "foo")); + + await That(result.MembersToIgnore).IsEmpty(); + await That(result.CustomOptions).ContainsKey(typeof(MyClass)) + .WhoseValue.IsEquivalentTo(new + { + MembersToIgnore = It.Is().That.HasCount(1), + }); + await That(result.ToString()).IsEqualTo(""" + - include public fields and properties + - for EquivalencyOptionsExtensionsTests.MyClass: + - include public fields and properties + - ignore members: [x => x == "foo"] + """); + } + + [Fact] + public async Task Generic_For_Ignoring_StringTypeAndMemberInfoPredicate_ShouldSetOptionForType() + { + EquivalencyOptions options = new(); + + EquivalencyOptions result = options.For(o + => o.Ignoring((n, t, f) => n.EndsWith("At") && t == typeof(DateTime) && f != null)); + + await That(result.MembersToIgnore).IsEmpty(); + await That(result.CustomOptions).ContainsKey(typeof(MyClass)) + .WhoseValue.IsEquivalentTo(new + { + MembersToIgnore = It.Is().That.HasCount(1), + }); + await That(result.ToString()).IsEqualTo(""" + - include public fields and properties + - for EquivalencyOptionsExtensionsTests.MyClass: + - include public fields and properties + - ignore members: [(n, t, f) => n.EndsWith("At") && t == typeof(DateTime) && f != null] + """); + } + + [Fact] + public async Task Generic_For_Ignoring_TypePredicate_ShouldSetOptionForType() + { + EquivalencyOptions options = new(); + + EquivalencyOptions result = options.For(o => o.Ignoring(x => x == typeof(DateTime))); + + await That(result.MembersToIgnore).IsEmpty(); + await That(result.CustomOptions).ContainsKey(typeof(MyClass)) + .WhoseValue.IsEquivalentTo(new + { + MembersToIgnore = It.Is().That.HasCount(1), + }); + await That(result.ToString()).IsEqualTo(""" + - include public fields and properties + - for EquivalencyOptionsExtensionsTests.MyClass: + - include public fields and properties + - ignore members: [x => x == typeof(DateTime)] + """); + } + [Theory] [InlineData(true)] [InlineData(false)] @@ -45,7 +131,7 @@ await That(result.CustomOptions).ContainsKey(typeof(MyClass)) { MembersToIgnore = new[] { - memberToIgnore, + new MemberToIgnore.ByName(memberToIgnore), }, }); await That(result.ToString()).IsEqualTo($""" diff --git a/Tests/aweXpect.Tests/Objects/ThatObject.IsEquivalentTo.Tests.cs b/Tests/aweXpect.Tests/Objects/ThatObject.IsEquivalentTo.Tests.cs index 028435678..a44884aa8 100644 --- a/Tests/aweXpect.Tests/Objects/ThatObject.IsEquivalentTo.Tests.cs +++ b/Tests/aweXpect.Tests/Objects/ThatObject.IsEquivalentTo.Tests.cs @@ -1,6 +1,7 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using System.Reflection; using aweXpect.Equivalency; // ReSharper disable UnusedMember.Local @@ -31,6 +32,143 @@ async Task Act() await That(Act).DoesNotThrow(); } + [Fact] + public async Task IgnoringMismatchingProperties_ShouldBeEquivalent() + { + OuterClass subject = new() + { + Value = "Foo", + Inner = new InnerClass + { + IntValue = 1, + }, + }; + OuterClass expected = new() + { + Value = "Foo", + Inner = new InnerClass + { + IntValue = 2, + }, + }; + + async Task Act() + => await That(subject).IsEquivalentTo(expected, o => o + .For(x => x.IgnoringMember("IntValue"))); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task IgnoringMismatchingPropertiesByPathAndTypePredicate_ShouldBeEquivalent() + { + OuterClass subject = new() + { + Value = "Foo", + Inner = new InnerClass + { + IntValue = 1, + }, + }; + OuterClass expected = new() + { + Value = "Foo", + Inner = new InnerClass + { + IntValue = 2, + }, + }; + + async Task Act() + => await That(subject).IsEquivalentTo(expected, o => o + .Ignoring((memberPath, memberType) + => memberPath.EndsWith("IntValue") && memberType == typeof(int))); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task IgnoringMismatchingPropertiesByPathPredicate_ShouldBeEquivalent() + { + OuterClass subject = new() + { + Value = "Foo", + Inner = new InnerClass + { + IntValue = 1, + }, + }; + OuterClass expected = new() + { + Value = "Foo", + Inner = new InnerClass + { + IntValue = 2, + }, + }; + + async Task Act() + => await That(subject).IsEquivalentTo(expected, o => o + .Ignoring(memberPath => memberPath == "Inner.IntValue")); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task IgnoringMismatchingPropertiesByPathTypeAndMemberInfoPredicate_ShouldBeEquivalent() + { + OuterClass subject = new() + { + Value = "Foo", + Inner = new InnerClass + { + IntValue = 1, + }, + }; + OuterClass expected = new() + { + Value = "Foo", + Inner = new InnerClass + { + IntValue = 2, + }, + }; + + async Task Act() + => await That(subject).IsEquivalentTo(expected, o => o + .Ignoring((memberPath, _, memberInfo) + => memberPath.EndsWith("IntValue") && memberInfo is PropertyInfo)); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task IgnoringMismatchingPropertiesByTypePredicate_ShouldBeEquivalent() + { + OuterClass subject = new() + { + Value = "Foo", + Inner = new InnerClass + { + IntValue = 1, + }, + }; + OuterClass expected = new() + { + Value = "Foo", + Inner = new InnerClass + { + IntValue = 2, + }, + }; + + async Task Act() + => await That(subject).IsEquivalentTo(expected, o => o + .Ignoring(memberType => memberType == typeof(int))); + + await That(Act).DoesNotThrow(); + } + [Fact] public async Task MismatchedObjects_ShouldNotBeEquivalent() { @@ -142,8 +280,10 @@ is equivalent to ThatObject.OuterClass { "4" ], Inner = , + IntValue = 0, Value = "Baz" }, + IntValue = 0, Value = "Bar" }, Value = "Foo" @@ -244,8 +384,10 @@ is equivalent to ThatObject.OuterClass { "4" ], Inner = , + IntValue = 0, Value = "Bart" }, + IntValue = 0, Value = "Bar" }, Value = "Foo" @@ -334,8 +476,10 @@ is equivalent to ThatObject.OuterClass { Inner = ThatObject.InnerClass { Collection = , Inner = , + IntValue = 0, Value = "Baz" }, + IntValue = 0, Value = "Bar" }, Value = "Foo" @@ -1144,11 +1288,12 @@ is not equivalent to ThatObject.OuterClass { Inner = ThatObject.InnerClass { Collection = , Inner = , + IntValue = 0, Value = }, Value = "Foo" } - + Equivalency options: - include public fields and properties - ignore members: ["Inner"] diff --git a/Tests/aweXpect.Tests/Objects/ThatObject.IsNotEquivalentTo.Tests.cs b/Tests/aweXpect.Tests/Objects/ThatObject.IsNotEquivalentTo.Tests.cs index f4988535c..4b07a9388 100644 --- a/Tests/aweXpect.Tests/Objects/ThatObject.IsNotEquivalentTo.Tests.cs +++ b/Tests/aweXpect.Tests/Objects/ThatObject.IsNotEquivalentTo.Tests.cs @@ -106,8 +106,10 @@ is not equivalent to ThatObject.OuterClass { "4" ], Inner = , + IntValue = 0, Value = "Baz" }, + IntValue = 0, Value = "Bar" }, Value = "Foo" @@ -122,13 +124,15 @@ is not equivalent to ThatObject.OuterClass { "3" ], Inner = , + IntValue = 0, Value = "Baz" }, + IntValue = 0, Value = "Bar" }, Value = "Foo" } - + Equivalency options: - include public fields and properties - ignore members: ["Inner.Inner.Collection[3]"] @@ -214,8 +218,10 @@ is not equivalent to ThatObject.OuterClass { Inner = ThatObject.InnerClass { Collection = , Inner = , + IntValue = 0, Value = "Baz" }, + IntValue = 0, Value = "Bar" }, Value = "Foo" @@ -226,13 +232,15 @@ is not equivalent to ThatObject.OuterClass { Inner = ThatObject.InnerClass { Collection = , Inner = , + IntValue = 0, Value = "Baz" }, + IntValue = 0, Value = "Bar" }, Value = "Foo" } - + Equivalency options: - include public fields and properties """); @@ -284,8 +292,10 @@ is not equivalent to ThatObject.OuterClass { "3" ], Inner = , + IntValue = 0, Value = "Baz" }, + IntValue = 0, Value = "Bar" }, Value = "Foo" @@ -300,13 +310,15 @@ is not equivalent to ThatObject.OuterClass { "3" ], Inner = , + IntValue = 0, Value = "Baz" }, + IntValue = 0, Value = "Bar" }, Value = "Foo" } - + Equivalency options: - include public fields and properties """); diff --git a/Tests/aweXpect.Tests/Objects/ThatObject.cs b/Tests/aweXpect.Tests/Objects/ThatObject.cs index c69cb773d..474b6f82a 100644 --- a/Tests/aweXpect.Tests/Objects/ThatObject.cs +++ b/Tests/aweXpect.Tests/Objects/ThatObject.cs @@ -21,6 +21,8 @@ private sealed class InnerClass public InnerClass? Inner { get; set; } public string? Value { get; set; } + + public int IntValue { get; set; } } private sealed class OuterClass From 6df4903d21d0cf13c832d05074aff00b3ffbc3c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Wed, 3 Sep 2025 17:13:06 +0200 Subject: [PATCH 2/2] Switch to CoreOnly --- Pipeline/Build.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipeline/Build.cs b/Pipeline/Build.cs index a07a0cc08..25356ef59 100644 --- a/Pipeline/Build.cs +++ b/Pipeline/Build.cs @@ -19,7 +19,7 @@ partial class Build : NukeBuild /// /// Afterward, you can update the package reference in `Directory.Packages.props` and reset this flag. /// - readonly BuildScope BuildScope = BuildScope.Default; + readonly BuildScope BuildScope = BuildScope.CoreOnly; [Parameter("Github Token")] readonly string GithubToken;