如何测试复杂对象图的相等性?

假设我有一个unit testing,想要比较两个复杂的对象是否相等。 对象包含许多其他深层嵌套的对象。 所有对象的类都有正确定义的equals()方法。

这并不困难:

 @Test public void objectEquality() { Object o1 = ... Object o2 = ... assertEquals(o1, o2); } 

麻烦的是,如果对象不相等,你得到的只是失败,没有迹象表明对象图的哪个部分不匹配。 调试这可能是痛苦和令人沮丧的。

我目前的方法是确保一切都实现toString() ,然后像这样比较相等:

  assertEquals(o1.toString(), o2.toString()); 

这使得更容易追踪测试失败,因为像Eclipse这样的IDE有一个特殊的可视比较器,用于在失败的测试中显示字符串差异。 基本上,对象图以文本方式表示,因此您可以看到差异的位置。 只要toString()编写得很好,就可以了。

不过,这有点笨拙。 有时你想设计toString()用于其他目的,比如日志记录,也许你只想渲染一些对象字段而不是所有对象字段,或者根本没有定义toString(),依此类推。

我正在寻找更好的比较复杂对象图的方法。 有什么想法吗?

您可以做的是使用XStream将每个对象渲染为XML,然后使用XMLUnit对XML进行比较。 如果它们不同,那么您将获得上下文信息(以XPath,IIRC的forms),告诉您对象的不同之处。

例如,来自XMLUnit doc:

 Comparing test xml to control xml [different] Expected element tag name 'uuid' but was 'localId' - comparing  at /msg[1]/uuid[1] to  at /msg[1]/localId[1] 

请注意XPath指示不同元素的位置。

可能不快,但这可能不是unit testing的问题。

Atlassian Developer Blog上有一些关于这个主题的文章,以及Hamcrest库如何能够非常简单地调试这种测试失败:

  • Hamcrest如何拯救你的灵魂 (第1部分)
  • Hamcrest拯救了你的灵魂 – 现在减少了痛苦! (第2部分)

基本上,对于这样的断言:

 assertThat(lukesFirstLightsaber, is(equalTo(maceWindusLightsaber))); 

Hamcrest将像这样返回输出(其中只显示不同的字段):

 Expected: is {singleBladed is true, color is PURPLE, hilt is {...}} but: is {color is GREEN} 

由于我倾向于设计复杂对象的方式,我在这里有一个非常简单的解决方案。

在设计一个我需要编写equals方法的复杂对象(因此也就是hashCode方法)时,我倾向于编写一个字符串渲染器,并使用String类equals和hashCode方法。

当然,渲染器不是为了字符串:它对人类来说并不是很容易阅读,并且包含了我需要比较的所有和唯一的值,并且通过习惯我把它们按照控制方式的顺序排列我希望他们排序; 这些都不一定适用于toString方法。

当然,我缓存这个渲染的字符串(以及hashCode值)。 它通常是私有的,但是让缓存的字符串package-private可以让你从unit testing中看到它。

顺便说一句,这并不总是我在交付系统中最终得到的结果 – 当然,如果性能测试表明这种方法太慢,我准备更换它,但这是一种罕见的情况。 到目前为止,它只发生过一次,在一个系统中,可变对象被迅速改变并经常进行比较。

我这样做的原因是编写一个好的hashCode并不简单 ,并且需要测试(*),而使用String中的那个可以避免测试。

(*考虑到Josh Bloch撰写一个好的hashCode方法的步骤中的第3步是测试它以确保“相等”对象具有相等的hashCode值,并确保覆盖所有可能的变化被覆盖并不是微不足道的本身。更精细,更难以测试的是分布)

http://code.google.com/p/deep-equals/上存在此问题的代码

使用DeepEquals.deepEquals(a,b)比较两个Java对象的语义相等性。 这将使用它们可能具有的任何自定义equals()方法来比较对象(如果它们具有除Object.equals()之外实现的equals()方法)。 如果不是,则此方法将继续逐个字段地逐个比较对象。 遇到每个字段时,如果存在,它将尝试使用派生的equals(),否则它将继续进一步递归。

此方法将在循环对象图上工作,如下所示:A-> B-> C-> A. 它具有循环检测function,因此可以比较任何两个对象,它永远不会进入无限循环。

使用DeepEquals.hashCode(obj)计算任何对象的hashCode()。 与deepEquals()一样,如果实现了自定义hashCode()方法(在Object.hashCode()下面),它将尝试调用hashCode()方法,否则它将按字段递归(深度)计算hashCode字段。 与deepEquals()一样,此方法将处理带循环的Object图。 例如,A-> B-> C-> A. 在这种情况下,hashCode(A)== hashCode(B)== hashCode(C)。 DeepEquals.deepHashCode()具有循环检测function,因此可以在任何对象图上运行。

unit testing应该有明确定义的单一测试。 这意味着最终你应该有明确的, 单一的东西,这两个对象可以是不同的。 如果有太多可能不同的事情,我建议将此测试分成几个较小的测试。

我跟着你走的同一条赛道。 我还有其他麻烦:

  • 我们不能修改我们不拥有的类(对于equals或toString)(JDK),数组等。
  • 在各种情况下,平等有时是不同的

例如,跟踪实体相等性可能依赖于数据库ID(“同一行”概念),依赖于某些字段(业务键)的相等性(对于未保存的对象)。 对于Junit断言,您可能希望所有字段都相等。


所以我最终创建了通过图形运行的对象,随时随地完成工作。

通常有一个超类Crawling对象:

  • 爬行对象的所有属性; 停在:

    • 枚举,
    • 框架类(如果适用),
    • 在卸载的代理或远程连接,
    • 已经访问过的对象(以避免循环)
    • 在多对一关系中,如果它们表示父母(通常不包括在等于语义中)
  • 可配置,以便它可以在某个时刻停止(完全停止,或停止在当前属性内爬行):

    • 当mustStopCurrent()或mustStopCompletely()方法返回true时,
    • 当在getter或类上遇到一些注释时,
    • 当前(class,getter)属于exception列表时

从那个Crawling超类中,子类可以满足许多需求:

  • 用于创建调试字符串 (根据需要调用toString,具有集合的特殊情况和没有良好toString的数组;处理大小限制等等)。
  • 用于创建多个均衡器 (如前所述,对于使用ID的实体,对于所有字段,或仅基于等于;)。 这些均衡器通常也需要特殊情况(例如,对于您无法控制的类)。

回到问题:这些均衡器可以记住不同值的路径 ,这对于您的JUnit案例来理解差异非常有用。

  • 用于创建Orderers 。 例如,需要完成的保存实体是一个特定的顺序,效率将决定将相同的类保存在一起将带来巨大的推动。
  • 用于收集可在图形中的各个级别找到的一组对象。 然后循环收集器的结果非常容易。

作为补充,我必须说,除了性能是真正关注的实体之外,我确实选择了在我的实体上实现toString(),hashCode(),equals()和compareTo()的技术。

例如,如果通过类上的@UniqueConstraint在Hibernate中定义了一个或多个字段上的业务键,那么让我们假设我的所有实体都有一个在公共超类中实现的getIdent()属性。 我的实体超类具有这四种方法的默认实现,这些方法依赖于这些知识,例如(需要处理空值):

  • toString()打印“myClass(key1 = value1,key2 = value2)”
  • hashCode()是“value1.hashCode()^ value2.hashCode()”
  • equals()是“value1.equals(other.value1)&& value2.equals(other.value2)”
  • compareTo()结合了class,value1和value2的比较。

对于性能受到关注的实体,我只是覆盖这些方法以不使用reflection。 我可以在回归JUnit测试中测试两个实现的行为相同。

我们使用一个名为junitx的库来测试所有“常见”对象的equals契约: http : //www.extreme-java.de/junitx/

我可以想到测试equals()方法的不同部分的唯一方法是将信息细分为更细粒度的东西。 如果您正在测试深度嵌套的对象树,那么您所做的并不是真正的unit testing。 您需要使用针对该类型对象的单独测试用例来测试图中每个单独对象的equals()契约。 对于被测对象上的类类型字段,可以使用带有简单equals()实现的存根对象。

HTH

我不会使用toString()因为正如您所说,为了显示或记录目的,创建对象的漂亮表示通常更有用。

听起来我的“单位”测试并没有隔离被测单元。 例如,如果您的对象图是A-->B-->C并且您正在测试A ,则A的unit testing不应该关心C中的equals()方法是否正常工作。 您对Cunit testing将确保它有效。

因此,我将在Aequals()方法的测试中测试以下内容: – 在两个方向上比较两个具有相同B的A对象,例如a1.equals(a2)a2.equals(a1) 。 – 在两个方向上比较两个具有不同BA对象

通过这种方式,通过每个比较的JUnit断言,您将知道失败的位置。

显然,如果你的class级有更多的孩子是决定平等的一部分,你需要测试更多的组合。 我试图得到的是,你的unit testing不应该关心它直接接触的类之外的任何行为。 在我的例子中,这意味着,你会假设C.equals()正常工作。

如果您要比较集合,可能会有一个问题。 在这种情况下,我将使用实用程序来比较集合,例如commons-collections CollectionUtils.isEqualCollection() 。 当然,仅适用于您所测试单元中的集合。

如果您愿意用scala编写测试,可以使用matchete 。 它是一组匹配器,可以与JUnit一起使用,并提供比较对象图的能力:

 case class Person(name: String, age: Int, address: Address) case class Address(street: String) Person("john",12, Address("rue de la paix")) must_== Person("john",12,Address("rue du bourg")) 

将产生以下错误消息

 org.junit.ComparisonFailure: Person(john,12,Address(street)) is not equal to Person(john,12,Address(different street)) Got : address.street = 'rue de la paix' Expected : address.street = 'rue du bourg' 

正如你在这里看到的,我一直在使用case类,这些类被matchete识别,以便深入到对象图中。 这是通过名为Diffable的类型类Diffable 。 我不打算在这里讨论类型类,所以让我们说它是这种机制的基石,它比较了给定类型的2个实例。 不是case-classes的类型(基本上所有Java类型)都获得一个使用equals的默认Diffable 。 除非您为特定类型提供Diffable ,否则这不是很有用:

 // your java object public class Person { public String name; public Address address; } 
 // you scala test code implicit val personDiffable : Diffable[Person] = Diffable.forFields(_.name,_.address) // there you go you can now compare two person exactly the way you did it // with the case classes 

所以我们已经看到matchete适用于java代码库。 事实上,我在上一份大型Java项目工作中一直使用matchete。

免责声明:我是matchete作者:)