【问题标题】:How to Compare two objects in unit test?如何在单元测试中比较两个对象?
【发布时间】:2022-03-05 00:02:28
【问题描述】:
public class Student
{
    public string Name { get; set; }
    public int ID { get; set; }
}

...

var st1 = new Student
{
    ID = 20,
    Name = "ligaoren",
};

var st2 = new Student
{
    ID = 20,
    Name = "ligaoren",
};

Assert.AreEqual<Student>(st1, st2);// How to Compare two object in Unit test?

如何比较 Unitest 中的两个集合?

【问题讨论】:

  • 您是在问比较对象的相等性并测试对象是否相等?

标签: c# unit-testing


【解决方案1】:

您要查找的是 xUnit Test Patterns 中称为 Test-Specific Equality 的内容。

虽然您有时可以选择覆盖 Equals 方法,但这可能会导致Equality Pollution,因为您需要测试的实现可能不是一般类型的正确实现。

例如,Domain-Driven Design 区分 实体值对象,它们的相等语义大不相同。

在这种情况下,您可以为相关类型编写自定义比较。

如果您厌倦了这样做,AutoFixture 的 Likeness 类提供了通用的特定于测试的平等。对于您的 Student 课程,这将允许您编写这样的测试:

[TestMethod]
public void VerifyThatStudentAreEqual()
{
    Student st1 = new Student();
    st1.ID = 20;
    st1.Name = "ligaoren";

    Student st2 = new Student();
    st2.ID = 20;
    st2.Name = "ligaoren";

    var expectedStudent = new Likeness<Student, Student>(st1);

    Assert.AreEqual(expectedStudent, st2);
}

这不需要您覆盖 Student 上的 Equals。

Likeness 进行语义比较,因此它也可以比较两种不同的类型,只要它们在语义上相似。

【讨论】:

  • 我以前从未听说过 AutoFixture,这是一个有趣的项目!至于平等污染,我理解你的观点,但从我的角度来看,正是因为平等可能是模棱两可的,所以值得在测试中(痛苦地)明确。
  • 是的,但是你仍然需要在可用性和可维护性之间取得平衡,比较两个深度图的相等性对我来说太痛苦了。 Likeness 的好处在于它的使用非常明确,一旦你知道它是什么,它仍然非常轻量。
  • 我发现using Likeness 上的@MarkSeemann 博客文章非常有用。
  • 相似之处也可以在SemanticComparison NuGet package中找到
  • @VivekPatel 如果您有新问题,请提出新问题。在这里给我一个通知,如果可以的话,我很乐意回答。
【解决方案2】:

如果比较公共成员足以满足您的用例,只需将您的对象塞入 JSON 并比较结果字符串:

var js = new JavaScriptSerializer();
Assert.AreEqual(js.Serialize(st1), js.Serialize(st2));

JavaScriptSerializer Class

优点

  • 需要最少的代码、零工作且无需初步设置
  • 处理具有嵌套对象的复杂结构
  • 不会使用特定于单元测试的代码污染您的类型,例如 Equals

缺点

  • 只考虑可序列化的公共成员(但无需注释您的成员)
  • 不处理循环引用

【讨论】:

    【解决方案3】:

    您应该提供Object.EqualsObject.GetHashCode 中的override

    public override bool Equals(object obj) {
        Student other = obj as Student;
        if(other == null) {
            return false;
        }
        return (this.Name == other.Name) && (this.ID == other.ID);
    }
    
    public override int GetHashCode() {
        return 33 * Name.GetHashCode() + ID.GetHashCode();
    }
    

    至于检查两个集合是否相等,使用Enumerable.SequenceEqual

    // first and second are IEnumerable<T>
    Assert.IsTrue(first.SequenceEqual(second)); 
    

    请注意,您可能需要使用接受IEqualityComparer&lt;T&gt;overload

    【讨论】:

    • @Michael Haren:谢谢!我的愚蠢疏忽。
    • 重写 Equals 的问题在于它并不总是正确的做法。此处发布的实现暗示了值对象语义,这对于 DDD 值对象可能是正确的,但对于 DDD 实体则不正确(仅举几个一般示例)。
    • 最糟糕的事情是覆盖 Equals/GetHashCode 进行测试。过去在遇到麻烦后是否同意需要测试特定相等性。
    【解决方案4】:

    这是我们用于比较复杂图形的 NUnit 2.4.6 自定义约束。它支持嵌入式集合、父引用、设置数值比较的容差、识别要忽略的字段名称(甚至在层次结构中)以及始终忽略的修饰类型。

    我确信这段代码可以在 NUnit 之外使用,大部分代码不依赖于 NUnit。

    我们在数千个单元测试中使用它。

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Reflection;
    using System.Text;
    using NUnit.Framework;
    using NUnit.Framework.Constraints;
    
    namespace Tests
    {
        public class ContentsEqualConstraint : Constraint
        {
            private readonly object expected;
            private Constraint failedEquality;
            private string expectedDescription;
            private string actualDescription;
    
            private readonly Stack<string> typePath = new Stack<string>();
            private string typePathExpanded;
    
            private readonly HashSet<string> _ignoredNames = new HashSet<string>();
            private readonly HashSet<Type> _ignoredTypes = new HashSet<Type>();
            private readonly LinkedList<Type> _ignoredInterfaces = new LinkedList<Type>();
            private readonly LinkedList<string> _ignoredSuffixes = new LinkedList<string>();
            private readonly IDictionary<Type, Func<object, object, bool>> _predicates = new Dictionary<Type, Func<object, object, bool>>();
    
            private bool _withoutSort;
            private int _maxRecursion = int.MaxValue;
    
            private readonly HashSet<VisitedComparison> _visitedObjects = new HashSet<VisitedComparison>();
    
            private static readonly HashSet<string> _globallyIgnoredNames = new HashSet<string>();
            private static readonly HashSet<Type> _globallyIgnoredTypes = new HashSet<Type>();
            private static readonly LinkedList<Type> _globallyIgnoredInterfaces = new LinkedList<Type>();
    
            private static object _regionalTolerance;
    
            public ContentsEqualConstraint(object expectedValue)
            {
                expected = expectedValue;
            }
    
            public ContentsEqualConstraint Comparing<T>(Func<T, T, bool> predicate)
            {
                Type t = typeof (T);
    
                if (predicate == null)
                {
                    _predicates.Remove(t);
                }
                else
                {
                    _predicates[t] = (x, y) => predicate((T) x, (T) y);
                }
                return this;
            }
    
            public ContentsEqualConstraint Ignoring(string fieldName)
            {
                _ignoredNames.Add(fieldName);
                return this;
            }
    
            public ContentsEqualConstraint Ignoring(Type fieldType)
            {
                if (fieldType.IsInterface)
                {
                    _ignoredInterfaces.AddFirst(fieldType);
                }
                else
                {
                    _ignoredTypes.Add(fieldType);
                }
                return this;
            }
    
            public ContentsEqualConstraint IgnoringSuffix(string suffix)
            {
                if (string.IsNullOrEmpty(suffix))
                {
                    throw new ArgumentNullException("suffix");
                }
                _ignoredSuffixes.AddLast(suffix);
                return this;
            }
    
            public ContentsEqualConstraint WithoutSort()
            {
                _withoutSort = true;
                return this;
            }
    
            public ContentsEqualConstraint RecursingOnly(int levels)
            {
                _maxRecursion = levels;
                return this;
            }
    
            public static void GlobalIgnore(string fieldName)
            {
                _globallyIgnoredNames.Add(fieldName);
            }
    
            public static void GlobalIgnore(Type fieldType)
            {
                if (fieldType.IsInterface)
                {
                    _globallyIgnoredInterfaces.AddFirst(fieldType);
                }
                else
                {
                    _globallyIgnoredTypes.Add(fieldType);
                }
            }
    
            public static IDisposable RegionalIgnore(string fieldName)
            {
                return new RegionalIgnoreTracker(fieldName);
            }
    
            public static IDisposable RegionalIgnore(Type fieldType)
            {
                return new RegionalIgnoreTracker(fieldType);
            }
    
            public static IDisposable RegionalWithin(object tolerance)
            {
                return new RegionalWithinTracker(tolerance);
            }
    
            public override bool Matches(object actualValue)
            {
                typePathExpanded = null;
                actual = actualValue;
                return Matches(expected, actualValue);
            }
    
            private bool Matches(object expectedValue, object actualValue)
            {
    
                bool matches = true;
    
                if (!MatchesNull(expectedValue, actualValue, ref matches))
                {
                    return matches;
                }
                // DatesEqualConstraint supports tolerance in dates but works as equal constraint for everything else
                Constraint eq = new DatesEqualConstraint(expectedValue).Within(tolerance ?? _regionalTolerance);
                if (eq.Matches(actualValue))
                {
                    return true;
                }
    
                if (MatchesVisited(expectedValue, actualValue, ref matches))
                {
                    if (MatchesDictionary(expectedValue, actualValue, ref matches) &&
                        MatchesList(expectedValue, actualValue, ref matches) &&
                        MatchesType(expectedValue, actualValue, ref matches) &&
                        MatchesPredicate(expectedValue, actualValue, ref matches))
                    {
                        MatchesFields(expectedValue, actualValue, eq, ref matches);
                    }
                }
    
                return matches;
            }
    
            private bool MatchesNull(object expectedValue, object actualValue, ref bool matches)
            {
                if (IsNullEquivalent(expectedValue))
                {
                    expectedValue = null;
                }
    
                if (IsNullEquivalent(actualValue))
                {
                    actualValue = null;
                }
    
                if (expectedValue == null && actualValue == null)
                {
                    matches = true;
                    return false;
                }
    
                if (expectedValue == null)
                {
                    expectedDescription = "null";
                    actualDescription = "NOT null";
                    matches = Failure;
                    return false;
                }
    
                if (actualValue == null)
                {
                    expectedDescription = "not null";
                    actualDescription = "null";
                    matches = Failure;
                    return false;
                }
    
                return true;
            }
    
            private bool MatchesType(object expectedValue, object actualValue, ref bool matches)
            {
                Type expectedType = expectedValue.GetType();
                Type actualType = actualValue.GetType();
    
                if (expectedType != actualType)
                {
                    try
                    {
                        Convert.ChangeType(actualValue, expectedType);
                    }
                    catch(InvalidCastException)             
                    {
                        expectedDescription = expectedType.FullName;
                        actualDescription = actualType.FullName;
                        matches = Failure;
                        return false;
                    }
    
                }
                return true;
            }
    
            private bool MatchesPredicate(object expectedValue, object actualValue, ref bool matches)
            {
                Type t = expectedValue.GetType();
                Func<object, object, bool> predicate;
    
                if (_predicates.TryGetValue(t, out predicate))
                {
                    matches = predicate(expectedValue, actualValue);
                    return false;
                }
                return true;
            }
    
            private bool MatchesVisited(object expectedValue, object actualValue, ref bool matches)
            {
                var c = new VisitedComparison(expectedValue, actualValue);
    
                if (_visitedObjects.Contains(c))
                {
                    matches = true;
                    return false;
                }
    
                _visitedObjects.Add(c);
    
                return true;
            }
    
            private bool MatchesDictionary(object expectedValue, object actualValue, ref bool matches)
            {
                if (expectedValue is IDictionary && actualValue is IDictionary)
                {
                    var expectedDictionary = (IDictionary)expectedValue;
                    var actualDictionary = (IDictionary)actualValue;
    
                    if (expectedDictionary.Count != actualDictionary.Count)
                    {
                        expectedDescription = expectedDictionary.Count + " item dictionary";
                        actualDescription = actualDictionary.Count + " item dictionary";
                        matches = Failure;
                        return false;
                    }
    
                    foreach (DictionaryEntry expectedEntry in expectedDictionary)
                    {
                        if (!actualDictionary.Contains(expectedEntry.Key))
                        {
                            expectedDescription = expectedEntry.Key + " exists";
                            actualDescription = expectedEntry.Key + " does not exist";
                            matches = Failure;
                            return false;
                        }
                        if (CanRecurseFurther)
                        {
                            typePath.Push(expectedEntry.Key.ToString());
                            if (!Matches(expectedEntry.Value, actualDictionary[expectedEntry.Key]))
                            {
                                matches = Failure;
                                return false;
                            }
                            typePath.Pop();
                        }
                    }
                    matches = true;
                    return false;
                }
                return true;
            }
    
            private bool MatchesList(object expectedValue, object actualValue, ref bool matches)
            {
                if (!(expectedValue is IList && actualValue is IList))
                {
                    return true;
                }
    
                var expectedList = (IList) expectedValue;
                var actualList = (IList) actualValue;
    
                if (!Matches(expectedList.Count, actualList.Count))
                {
                    matches = false;
                }
                else
                {
                    if (CanRecurseFurther)
                    {
                        int max = expectedList.Count;
    
                        if (max != 0 && !_withoutSort)
                        {
                            SafeSort(expectedList);
                            SafeSort(actualList);
                        }
    
                        for (int i = 0; i < max; i++)
                        {
                            typePath.Push(i.ToString());
    
                            if (!Matches(expectedList[i], actualList[i]))
                            {
                                matches = false;
                                return false;
                            }
                            typePath.Pop();
                        }
                    }
                    matches = true;
                }
                return false;
            }
    
            private void MatchesFields(object expectedValue, object actualValue, Constraint equalConstraint, ref bool matches)
            {
                Type expectedType = expectedValue.GetType();
    
                FieldInfo[] fields = expectedType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic);
    
                // should have passed the EqualConstraint check
                if (expectedType.IsPrimitive ||
                    expectedType == typeof(string) ||
                    expectedType == typeof(Guid) ||
                    fields.Length == 0)
                {
                    failedEquality = equalConstraint;
                    matches = Failure;
                    return;
                }
    
                if (expectedType == typeof(DateTime))
                {
                    var expectedDate = (DateTime)expectedValue;
                    var actualDate = (DateTime)actualValue;
    
                    if (Math.Abs((expectedDate - actualDate).TotalSeconds) > 3.0)
                    {
                        failedEquality = equalConstraint;
                        matches = Failure;
                        return;
                    }
                    matches = true;
                    return;
                }
    
                if (CanRecurseFurther)
                {
                    while(true)
                    {
                        foreach (FieldInfo field in fields)
                        {
                            if (!Ignore(field))
                            {
                                typePath.Push(field.Name);
                                if (!Matches(GetValue(field, expectedValue), GetValue(field, actualValue)))
                                {
                                    matches = Failure;
                                    return;
                                }
                                typePath.Pop();
                            }
                        }
                        expectedType = expectedType.BaseType;
                        if (expectedType == null)
                        {
                            break;
                        }
                        fields = expectedType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic);
                    }
                }
                matches = true;
                return;
            }
    
            private bool Ignore(FieldInfo field)
            {
                if (_ignoredNames.Contains(field.Name) ||
                    _ignoredTypes.Contains(field.FieldType) ||
                    _globallyIgnoredNames.Contains(field.Name) ||
                    _globallyIgnoredTypes.Contains(field.FieldType) ||
                    field.GetCustomAttributes(typeof (IgnoreContentsAttribute), false).Length != 0)
                {
                    return true;
                }
    
                foreach(string ignoreSuffix in _ignoredSuffixes)
                {
                    if (field.Name.EndsWith(ignoreSuffix))
                    {
                        return true;
                    }
                }
    
                foreach (Type ignoredInterface in _ignoredInterfaces)
                {
                    if (ignoredInterface.IsAssignableFrom(field.FieldType))
                    {
                        return true;
                    }
                }
                return false;
            }
    
            private static bool Failure
            {
                get
                {
                    return false;
                }
            }
    
            private static bool IsNullEquivalent(object value)
            {
                return value == null ||
                        value == DBNull.Value ||
                       (value is int && (int) value == int.MinValue) ||
                       (value is double && (double) value == double.MinValue) ||
                       (value is DateTime && (DateTime) value == DateTime.MinValue) ||
                       (value is Guid && (Guid) value == Guid.Empty) ||
                       (value is IList && ((IList)value).Count == 0);
            }
    
            private static object GetValue(FieldInfo field, object source)
            {
                try
                {
                    return field.GetValue(source);
                }
                catch(Exception ex)
                {
                    return ex;
                }
            }
    
            public override void WriteMessageTo(MessageWriter writer)
            {
                if (TypePath.Length != 0)
                {
                    writer.WriteLine("Failure on " + TypePath);
                }
    
                if (failedEquality != null)
                {
                    failedEquality.WriteMessageTo(writer);
                }
                else
                {
                    base.WriteMessageTo(writer);
                }
            }
            public override void WriteDescriptionTo(MessageWriter writer)
            {
                writer.Write(expectedDescription);
            }
    
            public override void WriteActualValueTo(MessageWriter writer)
            {
                writer.Write(actualDescription);
            }
    
            private string TypePath
            {
                get
                {
                    if (typePathExpanded == null)
                    {
                        string[] p = typePath.ToArray();
                        Array.Reverse(p);
                        var text = new StringBuilder(128);
                        bool isFirst = true;
                        foreach(string part in p)
                        {
                            if (isFirst)
                            {
                                text.Append(part);
                                isFirst = false;
                            }
                            else
                            {
                                int i;
                                if (int.TryParse(part, out i))
                                {
                                    text.Append("[" + part + "]");
                                }
                                else
                                {
                                    text.Append("." + part);
                                }
                            }
                        }
                        typePathExpanded = text.ToString();
                    }
                    return typePathExpanded;
                }
            }
    
            private bool CanRecurseFurther
            {
                get
                {
                    return typePath.Count < _maxRecursion;
                }
            }
    
            private static bool SafeSort(IList list)
            {
                if (list == null)
                {
                    return false;
                }
    
                if (list.Count < 2)
                {
                    return true;
                }
    
                try
                {
                    object first = FirstNonNull(list) as IComparable;
                    if (first == null)
                    {
                        return false;
                    }
    
                    if (list is Array)
                    {
                        Array.Sort((Array)list);
                        return true;
                    }
                    return CallIfExists(list, "Sort");
                }
                catch
                {
                    return false;
                }
            }
    
            private static object FirstNonNull(IEnumerable enumerable)
            {
                if (enumerable == null)
                {
                    throw new ArgumentNullException("enumerable");
                }
                foreach (object item in enumerable)
                {
                    if (item != null)
                    {
                        return item;
                    }
                }
                return null;
            }
    
            private static bool CallIfExists(object instance, string method)
            {
                if (instance == null)
                {
                    throw new ArgumentNullException("instance");
                }
                if (String.IsNullOrEmpty(method))
                {
                    throw new ArgumentNullException("method");
                }
                Type target = instance.GetType();
                MethodInfo m = target.GetMethod(method, new Type[0]);
                if (m != null)
                {
                    m.Invoke(instance, null);
                    return true;
                }
                return false;
            }
    
            #region VisitedComparison Helper
    
            private class VisitedComparison
            {
                private readonly object _expected;
                private readonly object _actual;
    
                public VisitedComparison(object expected, object actual)
                {
                    _expected = expected;
                    _actual = actual;
                }
    
                public override int GetHashCode()
                {
                    return GetHashCode(_expected) ^ GetHashCode(_actual);
                }
    
                private static int GetHashCode(object o)
                {
                    if (o == null)
                    {
                        return 0;
                    }
                    return o.GetHashCode();
                }
    
                public override bool Equals(object obj)
                {
                    if (obj == null)
                    {
                        return false;
                    }
    
                    if (obj.GetType() != typeof(VisitedComparison))
                    {
                        return false;
                    }
    
                    var other = (VisitedComparison) obj;
                    return _expected == other._expected &&
                           _actual == other._actual;
                }
            }
    
            #endregion
    
            #region RegionalIgnoreTracker Helper
    
            private class RegionalIgnoreTracker : IDisposable
            {
                private readonly string _fieldName;
                private readonly Type _fieldType;
    
                public RegionalIgnoreTracker(string fieldName)
                {
                    if (!_globallyIgnoredNames.Add(fieldName))
                    {
                        _globallyIgnoredNames.Add(fieldName);
                        _fieldName = fieldName;
                    }
                }
    
                public RegionalIgnoreTracker(Type fieldType)
                {
                    if (!_globallyIgnoredTypes.Add(fieldType))
                    {
                        _globallyIgnoredTypes.Add(fieldType);
                        _fieldType = fieldType;
                    }
                }
    
                public void Dispose()
                {
                    if (_fieldName != null)
                    {
                        _globallyIgnoredNames.Remove(_fieldName);
                    }
                    if (_fieldType != null)
                    {
                        _globallyIgnoredTypes.Remove(_fieldType);
                    }
                }
            }
    
            #endregion
    
            #region RegionalWithinTracker Helper
    
            private class RegionalWithinTracker : IDisposable
            {
                public RegionalWithinTracker(object tolerance)
                {
                    _regionalTolerance = tolerance;
                }
    
                public void Dispose()
                {
                    _regionalTolerance = null;
                }
            }
    
            #endregion
    
            #region IgnoreContentsAttribute
    
            [AttributeUsage(AttributeTargets.Field)]
            public sealed class IgnoreContentsAttribute : Attribute
            {
            }
    
            #endregion
        }
        public class DatesEqualConstraint : EqualConstraint
        {
            private readonly object _expected;
    
            public DatesEqualConstraint(object expectedValue) : base(expectedValue)
            {
                _expected = expectedValue;
            }
    
            public override bool Matches(object actualValue)
            {
                if (tolerance != null && tolerance is TimeSpan)
                {
                    if (_expected is DateTime && actualValue is DateTime)
                    {
                        var expectedDate = (DateTime) _expected;
                        var actualDate = (DateTime) actualValue;
                        var toleranceSpan = (TimeSpan) tolerance;
    
                        if ((actualDate - expectedDate).Duration() <= toleranceSpan)
                        {
                            return true;
                        }
                    }
                    tolerance = null;
                }
                return base.Matches(actualValue);
            }
        }
    }
    

    【讨论】:

    • 刚刚将所有这些复制到 VisualStudio10 中的一个新类中,tolerance 未定义。是否已在较新的 Nunit 版本或类似版本中删除?
    • @TankorSmash,是的,他们为 NUnit 更改了很多 API。如果您纯粹使用内置约束,您通常不会注意到,但是当您创建自定义约束时,它会影响几乎每个版本的自定义约束。
    【解决方案5】:

    看起来像 AutoFixture 的 Likeness 是我解决这个问题所需要的(感谢 Mark Seeman),但它不支持比较集合元素的相似度(关于此事有几个未解决的问题,但尚未解决)。

    我发现 Kellerman Software 的 CompareObjects 可以解决问题:

    https://github.com/GregFinzer/Compare-Net-Objects

    【讨论】:

    • 在比较集合中的项目时,this answer 可能很有用。它使用 AutoFixture 的 Likeness 也展示了它的轻量级代理功能。
    【解决方案6】:

    Mark Seeman 的回答涵盖了一般问题:测试相等性是一个单独的问题,因此代码应该在类本身之外。 (我以前没见过“平等污染”,但是那个)。此外,这是一个与您的单元测试项目隔离的问题。更好的是,在许多情况下它是一个“已解决的问题”:有许多可用的断言库允许您以任意数量的任意方式测试相等性。他提出了一个建议,尽管其中有许多已经涌现或在随后的几年中变得更加成熟。

    为此,让我建议Fluent Assertions。它具有许多用于各种比较的功能。在这种情况下,它会很简单:

    st1.ShouldBeEquivalentTo(st2); // before 5.0
    

    st1.Should().BeEquivalentTo(st2); // 5.0 and later
    

    【讨论】:

      【解决方案7】:

      http://www.infoq.com/articles/Equality-Overloading-DotNET

      这篇文章可能有用,我解决了这个问题只是使用 refcetion dump all 归档; 然后我们只需要比较两个字符串。

      代码在这里:

       /// <summary>
          /// output all properties and values of obj
          /// </summary>
          /// <param name="obj"></param>
          /// <param name="separator">default as ";"</param>
          /// <returns>properties and values of obj,with specified separator </returns>
          /// <Author>ligaoren</Author>
          public static string Dump(object obj, string separator)
          {
              try
              {
                  if (obj == null)
                  {
                      return string.Empty;
                  }
                  if (string.IsNullOrEmpty(separator))
                  {
                      separator = ";";
                  }
                  Type t = obj.GetType();
                  StringBuilder info = new StringBuilder(t.Name).Append(" Values : ");
                  foreach (PropertyInfo item in t.GetProperties())
                  {
                      object value = t.GetProperty(item.Name).GetValue(obj, null);
                      info.AppendFormat("[{0}:{1}]{2}", item.Name, value, separator);
                  }
                  return info.ToString();
              }
              catch (Exception ex)
              {
                  log.Error("Dump Exception", ex);
                  return string.Empty;
              }
          }
      

      【讨论】:

        【解决方案8】:

        我刚刚做了:

        Assert.AreEqual(Newtonsoft.Json.JsonConvert.SerializeObject(object1),
                        Newtonsoft.Json.JsonConvert.SerializeObject(object2));
        

        【讨论】:

          【解决方案9】:
          1. 您好,首先使用 Nuget PM 添加您的测试项目 Newtonsoft.Json

            PM> 安装包 Newtonsoft.Json -Version 10.0.3

          2. 然后添加测试文件

            using Newtonsoft.Json;
            
          3. 用法:

            Assert.AreEqual(
                JsonConvert.SerializeObject(expected),
                JsonConvert.SerializeObject(actual));
            

          【讨论】:

            【解决方案10】:

            您还可以使用 NFluent 和 this 语法来深入比较两个对象,而无需为您的对象实现相等性。 NFluent 是一个试图简化编写可读测试代码的库。

            Check.That(actual).HasFieldsWithSameValues(expected);
            

            此方法失败,异常包含所有差异,而不是第一个失败。我觉得这个功能是一个加分项。

            【讨论】:

            • 最新版本的 NFluent 如何做到这一点?方法IsDeepEqualTo 似乎不再存在!
            • 看起来方法应该是HasFieldsWithSameValues而不是IsDeepEqualTo...?
            【解决方案11】:

            如果您使用 NUnit,您可以使用此语法并专门为测试指定 IEqualityComparer

            [Test]
            public void CompareObjectsTest()
            {
                ClassType object1 = ...;
                ClassType object2 = ...;
                Assert.That( object1, Is.EqualTo( object2 ).Using( new MyComparer() ) );
            }
            
            private class MyComparer : IEqualityComparer<ClassType>
            {
                public bool Equals( ClassType x, ClassType y )
                {
                    return ....
                }
            
                public int GetHashCode( ClassType obj )
                {
                    return obj.GetHashCode();
                }
            }
            

            参见此处:Equal Constraint (NUnit 2.4 / 2.5)

            【讨论】:

              【解决方案12】:

              这就是我的工作:

              public static void AreEqualXYZ_UsageExample()
              {
                  AreEqualXYZ(actual: class1UnderTest, 
                      expectedBoolExample: true, 
                      class2Assert: class2 => Assert.IsNotNull(class2), 
                      class3Assert: class3 => Assert.AreEqual(42, class3.AnswerToEverything));
              }
              
              public static void AreEqualXYZ(Class1 actual,
                  bool expectedBoolExample,
                  Action<Class2> class2Assert,
                  Action<Class3> class3Assert)
              {
                  Assert.AreEqual(actual.BoolExample, expectedBoolExample);
              
                  class2Assert(actual.Class2Property);
                  class3Assert(actual.Class3Property);
              }
              

              HTH..

              【讨论】:

                【解决方案13】:

                简单地使用这个方法,如果你的类实例相同,它将检查并返回成功的结果。不是你的类引用类型。干杯:)

                private fun verifyYourObject(obj1: YourClass, clazz: Class<*>) {
                        assertNotNull(obj1)
                        assertThat(obj1, instanceOf(clazz))
                    }
                

                【讨论】:

                  【解决方案14】:

                  我想添加 Mark Seeman 发布的答案的另一个版本。

                  在我的代码中,我使用 Mark Seemans SemanticComparison Nuget 包(过去,此 Nuget 包是 AutoFixture 包的一部分,但现在它独立存在。)

                  这个包允许编写类似于下面示例中“断言”部分的代码,我将actualInstanceexpectedInstance 进行比较,它们都是DataClass 类型。

                  在示例中,当使用Without 方法比较DataClass 的两个实例时,我添加了一个可选步骤来排除ID 属性。

                      [Test]
                      public void CompareInstances_Test()
                      {
                          // Arrange: Instanciate a class the represents the instance with the properties you expect
                          DataClass expectedInstance = new DataClass();
                          // Set properties here
                  
                  
                          // Act: Call a method in the tested class that will produce the result you want to compare to
                          var subjectUnderTest = new SubjectUnderTest();
                          DataClass actualInstance = subjectUnderTest.CreateInstance();
                  
                  
                          // Assert: Compare the two instances, but ignore the "ID" property
                          expectedInstance.AsSource().OfLikeness<DataClass>().Without(x => x.ID).ShouldEqual(actualInstance);
                      }
                  

                  【讨论】:

                    【解决方案15】:

                    也许您需要在课程中添加public bool Equals(object o)

                    【讨论】:

                      【解决方案16】:
                      obj1.ToString().Equals(obj2.ToString())
                      

                      【讨论】:

                        猜你喜欢
                        • 1970-01-01
                        • 1970-01-01
                        • 2018-02-26
                        • 1970-01-01
                        • 1970-01-01
                        • 2011-01-13
                        • 1970-01-01
                        • 1970-01-01
                        • 1970-01-01
                        相关资源
                        最近更新 更多