[英]How to Compare two objects in unit test?
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中的兩個集合?
您正在尋找的是xUnit測試模式中稱為測試特定平等的內容 。
雖然您有時可以選擇覆蓋Equals方法,但這可能會導致Equality污染,因為測試所需的實現可能不是一般類型的正確實現。
例如, 域驅動設計區分實體和值對象 ,並且那些具有截然不同的等式語義。
在這種情況下,您可以為相關類型編寫自定義比較。
如果你厭倦這樣做, 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);
}
這不要求您在學生上覆蓋等於。
Likeness執行語義比較,因此只要它們在語義上相似,它也可以比較兩種不同的類型。
如果比較公共成員足以滿足您的用例,只需將對象插入JSON並比較結果字符串:
var js = new JavaScriptSerializer();
Assert.AreEqual(js.Serialize(st1), js.Serialize(st2));
優點
Equals
污染您的類型 缺點
您應該提供Object.Equals
和Object.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<T>
的重載 。
看起來很喜歡AutoFixture的Likeness就是我對這個問題所需要的(感謝Mark Seeman)但是它並不支持比較收集元素的相似性(在這個問題上有幾個未解決的問題,但它們尚未解決)。
我發現Kellerman Software的 CompareObjects可以解決問題:
這是我們用於比較復雜圖形的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);
}
}
}
http://www.infoq.com/articles/Equality-Overloading-DotNET
這篇文章可能很有用,我只是使用refcetion dump解決了這個問題; 然后我們只需要比較兩個字符串。
代碼在這里:
/// <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;
}
}
我已經做了:
Assert.AreEqual(Newtonsoft.Json.JsonConvert.SerializeObject(object1),
Newtonsoft.Json.JsonConvert.SerializeObject(object2));
Mark Seeman的答案涵蓋了一般性問題:測試相等性是一個單獨的問題,因此代碼應該是類本身的外部。 (我之前沒有見過“平等污染”,但那)。 此外,這是一個與您單元測試項目隔離的問題。 更好的是,在許多情況下它是一個“已解決的問題”:有許多可用的斷言庫允許您以任意數量的任意方式測試相等性。 他提出了一個問題,盡管有許多問題在這些年間如雨后春筍般涌現或變得更加成熟。
為此,我建議Fluent斷言 。 它具有許多用於各種比較的功能。 在這種情況下,它將非常簡單:
st1.ShouldBeEquivalentTo(st2); // before 5.0
要么
st1.Should().BeEquivalentTo(st2); // 5.0 and later
您好首先將您的測試項目Newtonsoft.Json與Nuget PM一起添加
PM> Install-Package Newtonsoft.Json -Version 10.0.3
然后添加測試文件
using Newtonsoft.Json;
用法:
Assert.AreEqual( JsonConvert.SerializeObject(expected), JsonConvert.SerializeObject(actual));
您還可以使用具有此語法的NFluent深入比較兩個對象,而不實現對象的相等性。 NFluent是一個試圖簡化可讀測試代碼編寫的庫。
Check.That(actual).HasFieldsWithSameValues(expected);
此方法失敗,異常包含所有差異,而不是在第一個失敗。 我發現這個功能是一個加號。
看看以下鏈接。 它是代碼項目的解決方案,我也使用過它。 它適用於比較NUnit和MSUnit中的對象
http://www.codeproject.com/Articles/22709/Testing-Equality-of-Two-Objects?msg=5189539#xx5189539xx
也許你需要在類中添加一個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,您可以使用此語法並專門為測試指定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())
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.