如何在unit testing中比较两个对象?

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中的两个集合?

你在找什么是在xUnittesting模式称为testing特定的平等 。

虽然您有时可以select重写Equals方法,但这可能会导致平等污染,因为您需要进行testing的实现可能不是一般types的正确方法。

例如, 域驱动devise区分实体值对象 ,而那些具有相当不同的相等语义。

在这种情况下,您可以为所讨论的types编写自定义比较。

如果你这样做累了, AutoFixture的相似类提供了通用的testing特定的平等。 通过你的学生课程,这将允许你写这样的testing:

 [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); } 

这并不要求你重写等于学生。

相似性执行语义比较,所以它也可以比较两种不同的types,只要它们在语义上相似。

您应该提供Object.EqualsObject.GetHashCodeoverride

 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<T>的重载 。

它看起来很喜欢AutoFixture的像是我需要的这个问题(感谢马克·西曼),但它不支持比较收集元素的相似性(有一些关于此事的公开问题,但他们还没有得到解决)。

我发现通过Kellerman软件的 CompareObject可以做到这一点:

http://comparenetobjects.codeplex.com/

下面是一个我们用来比较复杂graphics的NUnit 2.4.6自定义约束。 它支持embedded式集合,父引用,为数字比较设置宽容,标识要忽略的字段名称(甚至在层次结构内),以及装饰types始终被忽略。

我确定这个代码可以适应NUnit以外的地方,大部分代码不依赖于NUnit。

我们在数千个unit testing中使用这个。

 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); } } } 

如果比较公共成员足够你的用例,只需将你的对象插入JSON并比较结果string:

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

JavaScriptSerializer类

优点

  • 需要最less的代码,零工作量,没有初步设置
  • 处理嵌套对象的复杂结构
  • 不要使用unit testing特定的代码来污染你的types,比如Equals

缺点

  • 但是只有可序列化的公共成员才能被考虑(不需要注释你的成员)
  • 不处理循环引用

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

本文可能是有用的,我解决这个问题只是使用refcetion dump全部归档出来; 那么我们只需要比较两个string。

代码在这里:

  /// <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; } } 

您也可以使用NFluent与此语法来深入比较两个对象,而不必为您的对象实现相等性。 NFluent是一个试图简化编写可读的testing代码的库。

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

此方法失败,包含所有差异,而不是第一个失败的exception。 我觉得这个function是一个加号。

看看下面的链接。 它是一个代码项目的解决scheme,我也使用它。 对于比较NUnit和MSUnit中的对象,它工作正常

http://www.codeproject.com/Articles/22709/Testing-Equality-of-Two-Objects?msg=5189539#xx5189539xx

Mark Seeman的答案涵盖了一个普遍的问题:testing平等是一个单独的问题,因此代码应该是类本身的外部。 (以前我没有看过“平等污染”,但是)。 此外,这是一个孤立的问题unit testing项目。 更好的是,在很多情况下,这是一个“解决的问题”:有许多可用的断言库可以让您以任意数量的方式来testing相等性。 他提出了一个,虽然在这几年里有很多已经出现或者变得更加成熟。

为此,让我build议stream利的断言 。 它有各种各样的断言的能力。 在这种情况下,这将是非常简单的:

 st1.ShouldBeEquivalentTo(st2); 

也许你需要添加一个public bool Equals(object o)给这个类。

这就是我所做的:

 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 ..

如果您使用NUnit,则可以使用此语法,并专门为此testing指定一个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(); } } 

另请参见: 等同约束(NUnit 2.4 / 2.5)

 obj1.ToString().Equals(obj2.ToString()) 
  1. 你好,首先添加你的testing项目Newtonsoft.Json和Nuget PM

    PM> Install-Package Newtonsoft.Json -Version 10.0.3

  2. 然后添加testing文件

    使用Newtonsoft.Json;

  3. 用法:

    Assert.AreEqual(JsonConvert.SerializeObject(预期),JsonConvert.SerializeObject(实际));