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?
如何在单元测试中比较两个集合?
答案 0 :(得分:48)
您要找的是xUnit Test Patterns中Test-Specific Equality所称的内容。
虽然您有时可以选择覆盖Equals方法,但这可能会导致Equality Pollution,因为测试所需的实现可能不是该类型的正确实现。
例如,Domain-Driven Design区分实体和值对象,并且那些具有截然不同的等式语义。
在这种情况下,您可以为相关类型编写自定义比较。
如果你厌倦了这样做,AutoFixture的相似类提供了通用的特定测试平等。使用您的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执行语义比较,因此它也可以比较两种不同的类型,只要它们在语义上相似。
答案 1 :(得分:9)
如果比较公共成员对于您的用例就足够了,只需将对象插入JSON并比较生成的字符串:
var js = new JavaScriptSerializer();
Assert.AreEqual(js.Serialize(st1), js.Serialize(st2));
<强>赞成强>
Equals
<强>缺点强>
答案 2 :(得分:7)
您应提供override
Object.Equals
和Object.GetHashCode
:
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));
请注意,您可能需要使用接受overload的IEqualityComparer<T>
。
答案 3 :(得分:7)
看起来喜欢AutoFixture的Likeness就是我对这个问题所需要的(感谢Mark Seeman)但是它不支持比较收集元素的相似性(关于这个问题有几个未解决的问题但是它们还没有得到解决)。
我发现Kellerman Software的CompareObjects可以解决问题:
答案 4 :(得分:6)
这是我们用于比较复杂图形的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);
}
}
}
答案 5 :(得分:4)
http://www.infoq.com/articles/Equality-Overloading-DotNET
这篇文章可能很有用,我只是使用反射转储解决了这个问题全部归档; 然后我们只需要比较两个字符串。
代码在这里:
/// <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;
}
}
答案 6 :(得分:3)
我刚刚做了:
Assert.AreEqual(Newtonsoft.Json.JsonConvert.SerializeObject(object1),
Newtonsoft.Json.JsonConvert.SerializeObject(object2));
答案 7 :(得分:2)
Mark Seeman的答案涵盖了一般性问题:测试相等性是一个单独的问题,因此代码应该是类本身的外部。 (我之前没有见过“平等污染”,但那)。此外,这是一个与您单元测试项目隔离的问题。更好的是,在许多情况下它是一个“已解决的问题”:有许多可用的断言库允许您以任意数量的任意方式测试相等性。他提出了一个问题,尽管在这几年中有许多已经涌现或变得更加成熟。
为此,请允许我建议Fluent Assertions。它具有许多用于各种比较的功能。在这种情况下,它将非常简单:
st1.ShouldBeEquivalentTo(st2); // before 5.0
或
st1.Should().BeEquivalentTo(st2); // 5.0 and later
答案 8 :(得分:1)
您还可以使用此语法的NFluent 来深入比较两个对象,而不会为对象实现相等性。 NFluent是一个试图简化可读测试代码编写的库。
Check.That(actual).HasFieldsWithSameValues(expected);
此方法失败,异常包含所有差异,而不是在第一个失败。我发现这个功能是一个优点。
答案 9 :(得分:1)
查看以下链接。它是代码项目的解决方案,我也使用过它。它适用于比较NUnit和MSUnit中的对象
http://www.codeproject.com/Articles/22709/Testing-Equality-of-Two-Objects?msg=5189539#xx5189539xx
答案 10 :(得分:1)
您好首先将您的测试项目Newtonsoft.Json与Nuget PM
一起添加PM&GT;安装包Newtonsoft.Json -Version 10.0.3
然后添加测试文件
using Newtonsoft.Json;
用法:
Assert.AreEqual(
JsonConvert.SerializeObject(expected),
JsonConvert.SerializeObject(actual));
答案 11 :(得分:0)
也许您需要在课程中添加public bool Equals(object o)
。
答案 12 :(得分:0)
这就是我的所作所为:
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 :(得分:0)
如果您使用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();
}
}
答案 14 :(得分:-3)
obj1.ToString().Equals(obj2.ToString())