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

我试图断言一个对象是“等于”另一个对象。

这些对象只是一个具有一堆公共属性的类的实例。 是否有一种简单的方法让NUnit根据属性声明相等性?

这是我目前的解决scheme,但我认为可能有更好的东西:

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

我所要做的将与CollectionEquivalentConstraint一样,其中NUnitvalidation两个集合的内容是相同的。

覆盖.Equals你的对象和unit testing,你可以简单地这样做:

 Assert.AreEqual(LeftObject, RightObject); 

当然,这可能意味着你只是将所有的个体比较移动到.Equals方法,但是它可以让你重复使用这个实现来进行多个testing,而且如果对象能够自己和兄弟进行比较,也许是有意义的。

如果因任何原因无法覆盖Equals,则可以构build一个辅助方法,通过reflection遍历公共属性并断言每个属性。 像这样的东西:

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

不要仅仅为了testing目的而覆盖Equals。 这是繁琐的,并影响域逻辑。 代替,

使用JSON来比较对象的数据

没有额外的逻辑对象。 没有额外的testing任务。

只需使用这个简单的方法:

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

这似乎工作得很好。 testing运行器结果信息将显示JSONstring比较(对象图),以便您直接看到错误。

另外请注意! 如果你有更大的复杂对象,只想比较一下它们的部分,你可以使用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 }); } 

尝试FluentAssertions库:

 dto.ShouldHave(). AllProperties().EqualTo(customer); 

http://www.fluentassertions.com/

它也可以使用NuGet进行安装。

我不想重写Equals只是为了启用testing。 不要忘记,如果你重写了Equals,你也应该重写GetHashCode,否则如果你在字典中使用你的对象,你可能会得到意想不到的结果。

我喜欢上面的反思方法,因为它迎合了未来的财产增加。

然而,对于一个快速而简单的解决scheme来说,通常最简单的方法是创build一个testing对象是否相等的帮助程序方法,或者在您对testing保持私有的类上实现IEqualityComparer。 当使用IEqualityComparer解决scheme时,您不需要执行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."); } } 

我已经尝试了这里提到的几种方法。 大多数涉及序列化您的对象和做一个string比较。 虽然超级简单,一般来说非常有效,但是我发现当你遇到一个失败的时候,会有一些短暂的变化,而这样的事情会被报告:

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

弄清楚差异在哪里是一个痛苦,至less可以说。

通过FluentAssertions的对象图比较 (即a.ShouldBeEquivalentTo(b) ),你可以回想一下:

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

这太好了。 现在得到FluentAssertions ,以后会很高兴的(如果你喜欢这个,请在​​第一次提出FluentAssertions的时候提出dkl 的答案 )。

我同意ChrisYoxall – 纯粹为了testing目的而在你的主代码中实现Equals是不好的。

如果因为一些应用程序逻辑需要它而实现Equals,那么这很好,但是保持纯粹的纯粹的仅用于testing的代码就可以避免混乱(也可能与应用程序需要的testing语义相同)。

简而言之,只保留testing代码。

对于大多数类来说,使用reflection的属性的简单比较应该足够了,但是如果对象具有复杂的属性,则可能需要recursion。 如果以下参考,谨防循环参考或类似的。

狡猾

Max Wikstrom的JSON解决scheme(上图)对我来说是最有意义的,它很短,干净,最重要的是它可以工作。 就个人而言,我宁愿将JSON转换作为一个单独的方法来实现,并把这个断言放回unit testing中,像这样…

HELPER方法:

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

单位testing:

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

仅供参考 – 您可能需要在解决scheme中添加对System.Web.Extensions的引用。

在NUnit 2.4.2中添加的属性约束允许一个比OP原来更具可读性的解决scheme,并产生更好的失败信息。 这不是一般的,但是如果你不需要太多的课程,这是一个非常合适的解决scheme。

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

另一个select是通过实现NUnit抽象Constraint类来编写自定义约束。 用一个辅助类来提供一些语法糖,结果得到的testing代码是愉快简洁和可读的

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

举一个极端的例子,考虑具有“只读”成员的类,不是IEquatable ,即使您想要,也不能更改被testing的类:

 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 (对于实际值的叙述)是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); } } 

加上助手类:

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

我会build立@Juanma的答案。 不过,我相信这不应该用unit testing断言来实现。 这是一个非常有用的工具,在某些情况下可以使用非testing代码。

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

我的build议如下:

 /// <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是专门为将对象图转储为string表示而编写的,旨在编写简单的unit testing。

  • 它来自witg Assert方法,输出正确转义的string,容易复制粘贴到testing中来纠正它。
  • 它允许unit testing被自动重写
  • 它集成了所有的unit testing框架
  • 与JSON序列化不同,支持循环引用
  • 你可以很容易地过滤,所以只有部分types被倾倒

特定

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

您可以以types安全的方式,并使用自动完成的视觉工作室包括或排除领域。

  var printer = new Stateprinter(); printer.Configuration.Projectionharvester().Exclude<A>(x => xX, x => xY); var sut = new A { X = DateTime.Now, Name = "Charly" }; var expected = @"new A(){ Name = ""Charly""}"; printer.Assert.PrintIsSame(expected, sut); 

只需从Nuget安装ExpectedObjects,就可以轻松地比较两个对象的属性值,集合的每个对象值,两个合成对象的值以及匿名types的部分比较属性值。

我在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

这是一个非常古老的线程,但我想知道是否有没有答案build议NUnit.Framework.Is.EqualToNUnit.Framework.Is.NotEqualTo

如:

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

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

反序列化这两个类,并进行string比较。

编辑:完美的作品,这是我从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!

编辑三:这是行不通的。 我在testing中使用它。 但是,您必须按照testing中的代码添加项目的顺序将项目添加到集合属性。

看看下面的链接。 它是一个来自代码项目的解决scheme,我也使用它。 它可以很好地比较对象。

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

将两个string进行串联和比较

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

我已经写完了一个简单的expression式工厂:

 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