假设我有一个单元测试,想要比较两个复杂的对象是否相等。这些对象包含许多其他深层嵌套的对象。所有对象的类都有正确定义的equals()
方法。
这并不困难:
@Test
public void objectEquality() {
Object o1 = ...
Object o2 = ...
assertEquals(o1, o2);
}
麻烦的是,如果对象不相等,你得到的只是失败,没有迹象表明对象图的哪一部分不匹配。调试这可能是痛苦和令人沮丧的。
我目前的做法是确保所有内容都实现toString()
,然后像这样比较相等:
assertEquals(o1.toString(), o2.toString());
这样可以更轻松地跟踪测试失败,因为像Eclipse这样的IDE有一个特殊的可视比较器,用于在失败的测试中显示字符串差异。基本上,对象图以文本方式表示,因此您可以看到差异的位置。只要toString()
编写得很好,它就会很好用。
我正在寻找更好的比较复杂对象图的方法。有什么想法吗?
答案 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)
我按照你所使用的相同曲目。我也有其他麻烦:
例如,跟踪实体相等性可能依赖于数据库ID(“同一行”概念),依赖于某些字段(业务键)的相等性(对于未保存的对象)。对于Junit断言,您可能希望所有字段都相等。
所以我最终创建了通过图表运行的对象,随时随地完成工作。
通常有超类抓取对象:
抓取对象的所有属性;停在:
可配置,以便它可以在某个时刻停止(完全停止,或停止在当前属性内爬行):
从那个Crawling超类中,子类可以满足很多需求:
回到问题:这些均衡器可以记住不同值的路径,这对于了解JUnit情况非常有用。
作为补充,我必须说,除了性能是真正关注的实体之外,我确实选择了该技术来实现我的实体上的toString(),hashCode(),equals()和compareTo()。
例如,如果通过类上的@UniqueConstraint在Hibernate中定义了一个或多个字段上的业务键,那么让我们假设我的所有实体都在一个公共超类中实现了一个getIdent()属性。 我的实体超类具有这4种方法的默认实现,这些方法依赖于这些知识,例如(需要处理空值):
对于性能受到关注的实体,我只是覆盖这些方法以不使用反射。我可以在回归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对象,例如: B
和a1.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作者:)