比较 NUnit 中两个对象之间的相等性

我试图断言一个对象与另一个对象“相等”。

这些对象只是具有一系列公共属性的类的实例。有没有一种简单的方法让 NUnit 基于属性断言相等性?

这是我目前的解决方案,但我认为还有更好的办法:

Assert.AreEqual(LeftObject.Property1, RightObject.Property1)
Assert.AreEqual(LeftObject.Property2, RightObject.Property2)
Assert.AreEqual(LeftObject.Property3, RightObject.Property3)
...
Assert.AreEqual(LeftObject.PropertyN, RightObject.PropertyN)

我要做的是与 CollectionEquvalentConstraint 相同的精神,NUnit 在其中验证两个集合的内容是相同的。

98404 次浏览

重写。等于你的对象,在单元测试中你可以简单地这样做:

Assert.AreEqual(LeftObject, RightObject);

当然,这可能意味着将所有单独的比较移动到。方法,但是它允许您为多个测试重用该实现,并且如果对象应该能够将自己与兄弟对象进行比较,那么使用该实现可能是有意义的。

如果由于任何原因无法重写 Equals,可以构建一个帮助器方法,该方法通过反射迭代公共属性并断言每个属性。大概是这样:

public static class AssertEx
{
public static void PropertyValuesAreEquals(object actual, object expected)
{
PropertyInfo[] properties = expected.GetType().GetProperties();
foreach (PropertyInfo property in properties)
{
object expectedValue = property.GetValue(expected, null);
object actualValue = property.GetValue(actual, null);


if (actualValue is IList)
AssertListsAreEquals(property, (IList)actualValue, (IList)expectedValue);
else if (!Equals(expectedValue, actualValue))
Assert.Fail("Property {0}.{1} does not match. Expected: {2} but was: {3}", property.DeclaringType.Name, property.Name, expectedValue, actualValue);
}
}


private static void AssertListsAreEquals(PropertyInfo property, IList actualList, IList expectedList)
{
if (actualList.Count != expectedList.Count)
Assert.Fail("Property {0}.{1} does not match. Expected IList containing {2} elements but was IList containing {3} elements", property.PropertyType.Name, property.Name, expectedList.Count, actualList.Count);


for (int i = 0; i < actualList.Count; i++)
if (!Equals(actualList[i], expectedList[i]))
Assert.Fail("Property {0}.{1} does not match. Expected IList with element {1} equals to {2} but was IList with element {1} equals to {3}", property.PropertyType.Name, property.Name, expectedList[i], actualList[i]);
}
}

我不希望仅仅为了启用测试而重写 Equals。不要忘记,如果重写 Equals,也应该重写 GetHashCode,否则,如果在字典中使用对象,可能会得到意想不到的结果。

我非常喜欢上面的反射方法,因为它可以满足将来添加属性的需要。

然而,对于一个快速简单的解决方案来说,最简单的方法通常是创建一个帮助器方法来测试对象是否相等,或者在一个类上实现 IEqualityComparer,这个类对于测试来说是私有的。当使用 IEqualityComparer 解决方案时,您不需要为 GetHashCode 的实现操心。例如:

// Sample class.  This would be in your main assembly.
class Person
{
public string Name { get; set; }
public int Age { get; set; }
}


// Unit tests
[TestFixture]
public class PersonTests
{
private class PersonComparer : IEqualityComparer<Person>
{
public bool Equals(Person x, Person y)
{
if (x == null && y == null)
{
return true;
}


if (x == null || y == null)
{
return false;
}


return (x.Name == y.Name) && (x.Age == y.Age);
}


public int GetHashCode(Person obj)
{
throw new NotImplementedException();
}
}


[Test]
public void Test_PersonComparer()
{
Person p1 = new Person { Name = "Tom", Age = 20 }; // Control data


Person p2 = new Person { Name = "Tom", Age = 20 }; // Same as control
Person p3 = new Person { Name = "Tom", Age = 30 }; // Different age
Person p4 = new Person { Name = "Bob", Age = 20 }; // Different name.


Assert.IsTrue(new PersonComparer().Equals(p1, p2), "People have same values");
Assert.IsFalse(new PersonComparer().Equals(p1, p3), "People have different ages.");
Assert.IsFalse(new PersonComparer().Equals(p1, p4), "People have different names.");
}
}

我同意 ChrisYoxall 的观点——纯粹为了测试目的在主代码中实现 Equals 是不好的。

如果你实现 Equals 是因为某些应用程序逻辑需要它,那没关系,但是不要把纯测试代码弄得乱七八糟(检查相同的代码进行测试的语义可能与你的应用程序需要的不同)。

简而言之,将仅测试代码排除在类之外。

对于大多数类来说,使用反射对属性进行简单的浅层比较应该足够了,但是如果对象具有复杂的属性,则可能需要递归。如果有下列引用,要小心循环引用或类似的引用。

希尔

反序列化这两个类,并进行字符串比较。

编辑: 工作正常,这是我从 NUnit 获得的输出;

Test 'Telecom.SDP.SBO.App.Customer.Translator.UnitTests.TranslateEaiCustomerToDomain_Tests.TranslateNew_GivenEaiCustomer_ShouldTranslateToDomainCustomer_Test("ApprovedRatingInDb")' failed:
Expected string length 2841 but was 5034. Strings differ at index 443.
Expected: "...taClasses" />\r\n  <ContactMedia />\r\n  <Party i:nil="true" /..."
But was:  "...taClasses" />\r\n  <ContactMedia>\r\n    <ContactMedium z:Id="..."
----------------------------------------------^
TranslateEaiCustomerToDomain_Tests.cs(201,0): at Telecom.SDP.SBO.App.Customer.Translator.UnitTests.TranslateEaiCustomerToDomain_Tests.Assert_CustomersAreEqual(Customer expectedCustomer, Customer actualCustomer)
TranslateEaiCustomerToDomain_Tests.cs(114,0): at Telecom.SDP.SBO.App.Customer.Translator.UnitTests.TranslateEaiCustomerToDomain_Tests.TranslateNew_GivenEaiCustomer_ShouldTranslateToDomainCustomer_Test(String custRatingScenario)

编辑二: 这两个对象可以是相同的,但是属性序列化的顺序不同。因此,XML 是不同的。DOH!

编辑三: 这真的有用。我正在用它做测试。但是,必须按照测试代码添加项的顺序将项添加到集合属性中。

试试 FluentAssertions 流利的断言库:

dto.Should().BeEquivalentTo(customer)

它也可以使用 NuGet 安装。

不要仅仅为了测试目的而覆盖 Equals。它很乏味并且会影响域逻辑。 相反,

使用 JSON 比较对象的数据

对象上没有额外的逻辑。没有额外的测试任务。

只要用这个简单的方法:

public static void AreEqualByJson(object expected, object actual)
{
var serializer = new System.Web.Script.Serialization.JavaScriptSerializer();
var expectedJson = serializer.Serialize(expected);
var actualJson = serializer.Serialize(actual);
Assert.AreEqual(expectedJson, actualJson);
}

看起来效果不错。测试运行器的结果信息将显示包含的 JSON 字符串比较(对象图) ,这样您就可以直接看到出了什么问题。

还有注意!如果你有更大的复杂对象,只是想比较它们的一部分,你可以(对序列数据使用 LINQ)创建匿名对象来使用上面的方法。

public void SomeTest()
{
var expect = new { PropA = 12, PropB = 14 };
var sut = loc.Resolve<SomeSvc>();
var bigObjectResult = sut.Execute(); // This will return a big object with loads of properties
AssExt.AreEqualByJson(expect, new { bigObjectResult.PropA, bigObjectResult.PropB });
}

另一种选择是通过实现 NUnit 抽象 Constraint类来编写自定义约束。通过一个 helper 类来提供一些语法上的优点,所得到的测试代码非常简洁易读,例如。

Assert.That( LeftObject, PortfolioState.Matches( RightObject ) );

对于一个极端的例子,考虑一下具有“只读”成员的类,它不是 IEquatable,而且即使您想要更改测试中的类,也无法更改:

public class Portfolio // Somewhat daft class for pedagogic purposes...
{
// Cannot be instanitated externally, instead has two 'factory' methods
private Portfolio(){ }


// Immutable properties
public string Property1 { get; private set; }
public string Property2 { get; private set; }  // Cannot be accessed externally
public string Property3 { get; private set; }  // Cannot be accessed externally


// 'Factory' method 1
public static Portfolio GetPortfolio(string p1, string p2, string p3)
{
return new Portfolio()
{
Property1 = p1,
Property2 = p2,
Property3 = p3
};
}


// 'Factory' method 2
public static Portfolio GetDefault()
{
return new Portfolio()
{
Property1 = "\{\{NONE}}",
Property2 = "\{\{NONE}}",
Property3 = "\{\{NONE}}"
};
}
}

Constraint类的合同要求重写 MatchesWriteDescriptionTo(在不匹配的情况下,重写预期价值的叙述) ,但重写 WriteActualValueTo(重写实际价值的叙述)也是有意义的:

public class PortfolioEqualityConstraint : Constraint
{
Portfolio expected;
string expectedMessage = "";
string actualMessage = "";


public PortfolioEqualityConstraint(Portfolio expected)
{
this.expected = expected;
}


public override bool Matches(object actual)
{
if ( actual == null && expected == null ) return true;
if ( !(actual is Portfolio) )
{
expectedMessage = "<Portfolio>";
actualMessage = "null";
return false;
}
return Matches((Portfolio)actual);
}


private bool Matches(Portfolio actual)
{
if ( expected == null && actual != null )
{
expectedMessage = "null";
expectedMessage = "non-null";
return false;
}
if ( ReferenceEquals(expected, actual) ) return true;


if ( !( expected.Property1.Equals(actual.Property1)
&& expected.Property2.Equals(actual.Property2)
&& expected.Property3.Equals(actual.Property3) ) )
{
expectedMessage = expected.ToStringForTest();
actualMessage = actual.ToStringForTest();
return false;
}
return true;
}


public override void WriteDescriptionTo(MessageWriter writer)
{
writer.WriteExpectedValue(expectedMessage);
}
public override void WriteActualValueTo(MessageWriter writer)
{
writer.WriteExpectedValue(actualMessage);
}
}

加上 helper 类:

public static class PortfolioState
{
public static PortfolioEqualityConstraint Matches(Portfolio expected)
{
return new PortfolioEqualityConstraint(expected);
}


public static string ToStringForTest(this Portfolio source)
{
return String.Format("Property1 = {0}, Property2 = {1}, Property3 = {2}.",
source.Property1, source.Property2, source.Property3 );
}
}

示例用法:

[TestFixture]
class PortfolioTests
{
[Test]
public void TestPortfolioEquality()
{
Portfolio LeftObject
= Portfolio.GetDefault();
Portfolio RightObject
= Portfolio.GetPortfolio("\{\{GNOME}}", "\{\{NONE}}", "\{\{NONE}}");


Assert.That( LeftObject, PortfolioState.Matches( RightObject ) );
}
}

Max Wikstrom 的 JSON 解决方案(上图)对我来说最有意义,它简短、干净,而且最重要的是它可以工作。就个人而言,尽管我更愿意将 JSON 转换作为一个单独的方法来实现,并像这样将断言放回单元测试中..。

助手方法:

public string GetObjectAsJson(object obj)
{
System.Web.Script.Serialization.JavaScriptSerializer oSerializer = new System.Web.Script.Serialization.JavaScriptSerializer();
return oSerializer.Serialize(obj);
}

单位测试:

public void GetDimensionsFromImageTest()
{
Image Image = new Bitmap(10, 10);
ImageHelpers_Accessor.ImageDimensions expected = new ImageHelpers_Accessor.ImageDimensions(10,10);


ImageHelpers_Accessor.ImageDimensions actual;
actual = ImageHelpers_Accessor.GetDimensionsFromImage(Image);


/*USING IT HERE >>>*/
Assert.AreEqual(GetObjectAsJson(expected), GetObjectAsJson(actual));
}

FYI-您可能需要在解决方案中添加对 System.Web.Extended 的引用。

我将以“胡安玛”的回答为基础。但是,我认为这不应该用单元测试断言来实现。这是一个非常适合在某些情况下由非测试代码使用的实用程序。

我写了一篇关于 http://timoch.com/blog/2013/06/unit-test-equality-is-not-domain-equality/的文章

我的建议如下:

/// <summary>
/// Returns the names of the properties that are not equal on a and b.
/// </summary>
/// <param name="a"></param>
/// <param name="b"></param>
/// <returns>An array of names of properties with distinct
///          values or null if a and b are null or not of the same type
/// </returns>
public static string[] GetDistinctProperties(object a, object b) {
if (object.ReferenceEquals(a, b))
return null;
if (a == null)
return null;
if (b == null)
return null;


var aType = a.GetType();
var bType = b.GetType();


if (aType != bType)
return null;


var props = aType.GetProperties();


if (props.Any(prop => prop.GetIndexParameters().Length != 0))
throw new ArgumentException("Types with index properties not supported");


return props
.Where(prop => !Equals(prop.GetValue(a, null), prop.GetValue(b, null)))
.Select(prop => prop.Name).ToArray();
}

与 NUnit 一起使用它

Expect(ReflectionUtils.GetDistinctProperties(tile, got), Empty);

在不匹配时产生以下消息。

Expected: <empty>
But was:  < "MagmaLevel" >
at NUnit.Framework.Assert.That(Object actual, IResolveConstraint expression, String message, Object[] args)
at Undermine.Engine.Tests.TileMaps.BasicTileMapTests.BasicOperations() in BasicTileMapTests.cs: line 29

Https://github.com/kbilsted/stateprinter 专门用于将对象图转储为字符串表示,目的是编写简单的单元测试。

  • 它带有 Assert 方法,可以输出一个正确转义的字符串,很容易复制粘贴到测试中来纠正它。
  • 它允许自动重写 unittest
  • 它集成了所有的单元测试框架
  • 与 JSON 序列化不同,支持循环引用
  • 可以很容易地进行筛选,因此只转储类型的一部分

给予

class A
{
public DateTime X;
public DateTime Y { get; set; }
public string Name;
}

您可以以类型安全的方式,并使用可视化工作室的自动完成包含或排除字段。

  var printer = new Stateprinter();
printer.Configuration.Projectionharvester().Exclude<A>(x => x.X, x => x.Y);


var sut = new A { X = DateTime.Now, Name = "Charly" };


var expected = @"new A(){ Name = ""Charly""}";
printer.Assert.PrintIsSame(expected, sut);

我已经尝试了这里提到的几种方法。大多数涉及序列化对象和进行字符串比较。虽然超级简单,而且通常非常有效,但我发现当你失败了,像这样的事情就会被报告:

Expected string length 2326 but was 2342. Strings differ at index 1729.

至少可以说,找出差异在哪里是一件痛苦的事情。

使用 FluentAssertions 的 对象图比较对象图比较(即 a.ShouldBeEquivalentTo(b)) ,您可以得到以下结果:

Expected property Name to be "Foo" but found "Bar"

这样好多了。现在是 去找 FluentAssertions,以后你会很高兴(如果你对此投赞成票,请也对最初建议使用 FluentAssertions 的 DKL 的回答投赞成票)。

只要从 Nuget 安装 ExpectedObjects,就可以很容易地比较两个对象的属性值、每个集合的对象值、两个组合对象的值和匿名类型的部分比较属性值。

我在 github 上有一些例子: https://github.com/hatelove/CompareObjectEquals

下面是一些包含比较对象场景的例子:

    [TestMethod]
public void Test_Person_Equals_with_ExpectedObjects()
{
//use extension method ToExpectedObject() from using ExpectedObjects namespace to project Person to ExpectedObject
var expected = new Person
{
Id = 1,
Name = "A",
Age = 10,
}.ToExpectedObject();


var actual = new Person
{
Id = 1,
Name = "A",
Age = 10,
};


//use ShouldEqual to compare expected and actual instance, if they are not equal, it will throw a System.Exception and its message includes what properties were not match our expectation.
expected.ShouldEqual(actual);
}


[TestMethod]
public void Test_PersonCollection_Equals_with_ExpectedObjects()
{
//collection just invoke extension method: ToExpectedObject() to project Collection<Person> to ExpectedObject too
var expected = new List<Person>
{
new Person { Id=1, Name="A",Age=10},
new Person { Id=2, Name="B",Age=20},
new Person { Id=3, Name="C",Age=30},
}.ToExpectedObject();


var actual = new List<Person>
{
new Person { Id=1, Name="A",Age=10},
new Person { Id=2, Name="B",Age=20},
new Person { Id=3, Name="C",Age=30},
};


expected.ShouldEqual(actual);
}


[TestMethod]
public void Test_ComposedPerson_Equals_with_ExpectedObjects()
{
//ExpectedObject will compare each value of property recursively, so composed type also simply compare equals.
var expected = new Person
{
Id = 1,
Name = "A",
Age = 10,
Order = new Order { Id = 91, Price = 910 },
}.ToExpectedObject();


var actual = new Person
{
Id = 1,
Name = "A",
Age = 10,
Order = new Order { Id = 91, Price = 910 },
};


expected.ShouldEqual(actual);
}


[TestMethod]
public void Test_PartialCompare_Person_Equals_with_ExpectedObjects()
{
//when partial comparing, you need to use anonymous type too. Because only anonymous type can dynamic define only a few properties should be assign.
var expected = new
{
Id = 1,
Age = 10,
Order = new { Id = 91 }, // composed type should be used anonymous type too, only compare properties. If you trace ExpectedObjects's source code, you will find it invoke config.IgnoreType() first.
}.ToExpectedObject();


var actual = new Person
{
Id = 1,
Name = "B",
Age = 10,
Order = new Order { Id = 91, Price = 910 },
};


// partial comparing use ShouldMatch(), rather than ShouldEqual()
expected.ShouldMatch(actual);
}

参考文献:

  1. ExpectedObjects github
  2. ExpectedObjects 简介

在 NUnit 2.4.2中添加的属性约束 允许比 OP 的原始解决方案更具可读性的解决方案,并且它产生更好的故障消息。它不是通用的,但是如果你不需要为太多的类这样做,这是一个非常适当的解决方案。

Assert.That(ActualObject, Has.Property("Prop1").EqualTo(ExpectedObject.Prop1)
& Has.Property("Prop2").EqualTo(ExpectedObject.Prop2)
& Has.Property("Prop3").EqualTo(ExpectedObject.Prop3)
// ...

虽然没有实现 Equals的通用性强,但它确实提供了比

Assert.AreEqual(ExpectedObject, ActualObject);

这是一个相当老的线程,但我想知道是否有一个原因为什么没有答案建议 NUnit.Framework.Is.EqualToNUnit.Framework.Is.NotEqualTo

例如:

Assert.That(LeftObject, Is.EqualTo(RightObject));

还有

Assert.That(LeftObject, Is.Not.EqualTo(RightObject));

串化并比较两个字符串

Assert.Areequals (JSON.stringify (LeftObject) ,JSON.stringify (RightObject))

最后,我写了一个简单的表达工厂:

public static class AllFieldsEqualityComprision<T>
{
public static Comparison<T> Instance { get; } = GetInstance();


private static Comparison<T> GetInstance()
{
var type = typeof(T);
ParameterExpression[] parameters =
{
Expression.Parameter(type, "x"),
Expression.Parameter(type, "y")
};
var result = type.GetProperties().Aggregate<PropertyInfo, Expression>(
Expression.Constant(true),
(acc, prop) =>
Expression.And(acc,
Expression.Equal(
Expression.Property(parameters[0], prop.Name),
Expression.Property(parameters[1], prop.Name))));
var areEqualExpression = Expression.Condition(result, Expression.Constant(0), Expression.Constant(1));
return Expression.Lambda<Comparison<T>>(areEqualExpression, parameters).Compile();
}
}

用它:

Assert.That(
expectedCollection,
Is.EqualTo(actualCollection)
.Using(AllFieldsEqualityComprision<BusinessCategoryResponse>.Instance));

这非常有用,因为我必须比较这些对象的集合。你可以在其他地方使用这个比较:)

下面是要点和例子: https://gist.github.com/Pzixel/b63fea074864892f9aba8ffde312094f

我知道这是一个非常古老的问题,但是 NUnit 仍然没有对此的本地支持。然而,如果你喜欢 BDD 风格的测试(ala Jasmine) ,你可以使用 nExpect (喜欢·一个人为 https://github.com/fluffynuts/NExpect,可以从 NuGet 获得) ,它包含了深层次的相等测试。

(免责声明: 我是 NExpect 的作者)

下面是上面的一个答案的修改版本,可以和 Moq 一起使用:

public static class Helpers {


public static bool DeepCompare(this object actual, object expected) {
var properties = expected.GetType().GetProperties();
foreach (var property in properties) {
var expectedValue = property.GetValue(expected, null);
var actualValue = property.GetValue(actual, null);


if (actualValue == null && expectedValue == null) {
return true;
}


if (actualValue == null || expectedValue == null) {
return false;
}


if (actualValue is IList actualList) {
if (!AreListsEqual(actualList, (IList)expectedValue)) {
return false;
}
}
else if (IsValueType(expectedValue)) {
if(!Equals(expectedValue, actualValue)) {
return false;
}
}
else if (expectedValue is string) {
return actualValue is string && Equals(expectedValue, actualValue);
}
else if (!DeepCompare(expectedValue, actualValue)) {
return false;
}
                

}
return true;
}


private static bool AreListsEqual(IList actualList, IList expectedList) {
if (actualList == null && expectedList == null) {
return true;
}


if (actualList == null  || expectedList == null) {
return false;
}


if (actualList.Count != expectedList.Count) {
return false;
}


if (actualList.Count == 0) {
return true;
}


var isValueTypeOrString = IsValueType(actualList[0]) || actualList[0] is string;


if (isValueTypeOrString) {
for (var i = 0; i < actualList.Count; i++) {
if (!Equals(actualList[i], expectedList[i])) {
return false;
}
}
}
else {
for (var i = 0; i < actualList.Count; i++) {
if (!DeepCompare(actualList[i], expectedList[i])) {
return false;
}
}
}


return true;
}


private static bool IsValueType(object obj) {
return obj != null && obj.GetType().IsValueType;
}

当您需要比 It.IsAny<>更多的东西并希望在所有属性上进行匹配时,它可以用于在模拟类型上指定设置时匹配对象,如下所示:

_clientsMock.Setup(m => m.SearchClients(
It.Is<SearchClientsPayload>(x => x.DeepCompare(expectedRequest)))).Returns(expectedResponse);

当然,可以改进它以处理可枚举数和其他复杂场景。