一个单元应该如何测试hashCode-equals合同?


79

简而言之,根据Java的object.hashCode(),hashCode契约:

  1. 除非影响equals()的内容发生变化,否则哈希码不应更改
  2. equals()表示哈希码==

让我们假设主要是对不可变数据对象感兴趣-它们的信息在构造之后就不会改变,因此假定#1保持不变。留下#2:问题仅仅是确认是否等于隐含哈希码==之一。

显然,除非该集合很小,否则我们无法测试每个可能的数据对象。那么,编写可能会遇到常见情况的单元测试的最佳方法是什么?

由于此类的实例是不可变的,因此构造此类对象的方法有限。如果可能,此单元测试应涵盖所有这些内容。在我头顶上,切入点是子类的构造函数,反序列化和构造函数(应简化为构造函数调用问题)。

[我将尝试通过研究回答自己的问题。来自其他StackOverflowers的输入是此过程中受欢迎的安全机制。]

[这可能适用于其他OO语言,因此我要添加该标签。]


1
我通常会发现由于实施equals或hashcode而不是同时实现两者,合同已经失效。openpojo在Java项目中有帮助。EqualsAndHashCodeMatchRule可以帮助您。已经存在的答案提供了有关测试合同其余部分的足够详细信息。
Michiel Leegwater '19年

Answers:


73

EqualsVerifier是一个相对较新的开源项目,它在测试equals合同方面做得很好。它不存在来自GSBase的EqualsTester的问题。我肯定会推荐它。


7
只是使用EqualsVerifier教会了我一些有关Java的知识!
David

我疯了吗?EqualsVerifier似乎根本没有检查Value Object的语义!它认为我的类正确实现了equals(),但是我的类使用默认的“ ==”行为。怎么会这样?!我一定缺少简单的东西。
JB Rainsberger

啊哈!如果我不重写equals(),则EqualsVerifier假定一切正常!那不是我所期望的。我希望不覆盖equals()的行为与覆盖equals()的行为相同,从而实现super.equals()完全相同的功能。奇怪的。
JB Rainsberger,2015年

7
@JBRainsberger嗨!这里是EqualsVerifier的创建者。对不起,您感到困惑。回复:不覆盖等于:您可以使用“ allFieldsShouldBeUsed()”启用预期的行为。根据您的说明,这将是下一版本的默认设置。回复:相同的实例:我认为如果不看一些示例代码,我将无法为您提供帮助。您能否详细说明一个新问题,或在EV邮件列表上groups.google.com/forum/?fromgroups#!forum/equalsverifier上进行阐述?谢谢!
jqno 2015年

1
EqualsVerifier确实功能强大。通过不测试不相等对象的每个潜在组合,我获得了大量时间。错误消息确实准确且具有启发性。如果您的编码约定与默认期望不同,则验证程序是非常可配置的。很棒@jqno
贡塔德

11

我的建议是考虑为什么/如何可能不成立,然后编写针对这些情况的一些单元测试。

例如,假设您有一个自定义Set类。如果两个集合包含相同的元素,则两个集合相等。但是,如果两个元素的存储顺序不同,则两个相等集合的基础数据结构可能会不同。例如:

MySet s1 = new MySet( new String[]{"Hello", "World"} );
MySet s2 = new MySet( new String[]{"World", "Hello"} );
assertEquals(s1, s2);
assertTrue( s1.hashCode()==s2.hashCode() );

在这种情况下,集合中元素的顺序可能会影响它们的哈希,具体取决于您实现的哈希算法。因此,这就是我要编写的测试,因为它可以测试某种情况,即我知道某些散列算法可能会对我定义为相等的两个对象产生不同的结果。

无论您使用哪种自定义类,都应使用类似的标准。


6

值得为此使用junit插件。检查类EqualsHashCodeTestCase http://junit-addons.sourceforge.net/,可以扩展它并实现createInstance和createNotEqualInstance,这将检查equals和hashCode方法是否正确。


4

我会建议EqualsTester从GSBase。它基本上可以满足您的需求。我有两个(次要)问题:

  • 构造函数完成了所有工作,我认为这不是好习惯。
  • 当类A的实例等于类A的子类的实例时,它将失败。这不一定违反对等合同。

2

[在撰写本文时,还发布了另外三个答案。]

重申一下,我的问题的目的是找到测试的标准案例,以确认这一点hashCodeequals相互同意。我对这个问题的解决方法是想象程序员在编写有问题的类(即不可变数据)时采用的通用路径。例如:

  1. equals()不写就写hashCode()这通常意味着将相等定义为意味着两个实例的字段相等。
  2. hashCode()不写就写equals()这可能意味着程序员正在寻求更有效的哈希算法。

在#2的情况下,这个问题对我来说似乎不存在。没有其他实例equals(),因此不需要其他实例具有相等的哈希码。在最坏的情况下,哈希算法可能会导致哈希图的性能较差,这不在此问题的范围内。

在#1的情况下,标准单元测试需要使用传递给构造函数的相同数据创建同一对象的两个实例,并验证相等的哈希码。误报呢?仍然可以选择构造函数参数,这些参数恰好在不可靠的算法上产生相等的哈希码。倾向于避免使用此类参数的单元测试将满足此问题的实质。此处的快捷方式是检查源代码equals(),认真思考,然后在此基础上编写测试,但是尽管在某些情况下这可能是必需的,但也可能存在捕获常见问题的通用测试,并且这种测试也符合本着精神。这个问题。

例如,如果要测试的类(称为Data)具有一个采用String的构造函数,并且从String构造的equals()实例产生的实例为equals(),则好的测试可能会测试:

  • new Data("foo")
  • 另一个 new Data("foo")

我们甚至可以检查哈希码 new Data(new String("foo"))Data.equals()在我看来,,以强制不对String进行,尽管产生正确的哈希码比产生正确结果的可能性更大。

Eli Courtwright的答案是一个示例,它在基于equals规范知识的基础上努力思考一种打破哈希算法的方法。特殊集合的示例很不错,因为用户制作Collection的有时会出现,并且很容易在哈希算法中产生错误。


1

这是我在测试中会有多个断言的唯一情况之一。由于需要测试equals方法,因此还应该同时检查hashCode方法。因此,在每个equals方法测试用例上,还要检查hashCode协定。

A one = new A(...);
A two = new A(...);
assertEquals("These should be equal", one, two);
int oneCode = one.hashCode();
assertEquals("HashCodes should be equal", oneCode, two.hashCode());
assertEquals("HashCode should not change", oneCode, one.hashCode());

当然,检查良好的hashCode是另一项工作。老实说,我不会费心去做两次检查,以确保hashCode在同一运行中没有发生变化,通过在代码审查中捕获它并帮助开发人员了解为什么这不是一个好方法,可以更好地解决此类问题编写hashCode方法。



0

如果我有一个类Thing,那么和大多数其他人一样,我会写一个类ThingTest,其中包含该类的所有单元测试。每个ThingTest都有一个方法

 public static void checkInvariants(final Thing thing) {
    ...
 }

并且如果Thing该类重写hashCode并等于它具有一个方法

 public static void checkInvariants(final Thing thing1, Thing thing2) {
    ObjectTest.checkInvariants(thing1, thing2);
    ... invariants that are specific to Thing
 }

该方法负责检查旨在容纳在任何一对对象之间的所有不变式ThingObjectTest它委托给的方法负责检查必须在任何一对对象之间保持的所有不变式。与equalshashCode是所有对象的方法一样,该方法将检查hashCodeequals是一致的。

然后,我有一些测试方法可以创建Thing对象对,并将它们传递给逐对对象checkInvariants方法。我使用等价分区来确定哪些对值得测试。我通常将每个对创建为仅在一个属性上有所不同,再加上一个测试两个等效对象的测试。

有时我也有3自变量checkInvariants方法,尽管我发现它在findinf缺陷中用处不大,所以我不经常这样做


这类似于其他海报在这里提到的内容:stackoverflow.com/a/190989/545127
Raedwald 2013年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.