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

时间:2009-09-11 15:27:23

标签: java 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(),依此类推。

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

9 个答案:

答案 0 :(得分:10)

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

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

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

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

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

答案 1 :(得分:8)

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

e.g。来自XMLUnit doc:

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

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

可能不会很快,但这可能不是单元测试的问题。

答案 2 :(得分:4)

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

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

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

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

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

我这样做的原因是writing a good hashCode isn't trivial,并且需要测试(*),而使用String中的那个避免了测试。

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

答案 3 :(得分:3)

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

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

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

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

答案 4 :(得分:1)

我按照你所使用的相同曲目。我也有其他麻烦:

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

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


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

通常有超类抓取对象:

  • 抓取对象的所有属性;停在:

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

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

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

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

回到问题:这些均衡器可以记住不同值的路径,这对于了解JUnit情况非常有用。

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

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

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

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

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

答案 5 :(得分:1)

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

答案 6 :(得分:0)

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

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

HTH

答案 7 :(得分:0)

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

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

所以我会在A的{​​{1}}方法的测试中测试以下内容:   - 在两个方向上比较两个具有相同equals()个的A对象,例如: Ba1.equals(a2)。   - 比较两个方向上具有不同a2.equals(a1)的两个A个对象

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

显然,如果你的班级有更多的孩子是决定平等的一部分,你需要测试更多的组合。我试图得到的是,你的单元测试不应该关心它直接接触的类之外的任何行为。在我的示例中,这意味着,您可以假设B正常工作。

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

答案 8 :(得分:0)

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

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'

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